### Install @tldraw/sync
Source: https://tldraw.dev/docs/collaboration
Install the tldraw sync library to quickly add multiplayer functionality.
```bash
npm install @tldraw/sync
```
--------------------------------
### Install @tldraw/driver
Source: https://tldraw.dev/docs/driver
Install the driver package alongside tldraw using npm.
```bash
npm install @tldraw/driver
```
--------------------------------
### Install @tldraw/mermaid Package
Source: https://tldraw.dev/docs/mermaid
Install the `@tldraw/mermaid` package alongside `tldraw` using npm.
```bash
npm install @tldraw/mermaid
```
--------------------------------
### Initialize tldraw Editor in React
Source: https://tldraw.dev
This snippet shows how to integrate the Tldraw component into a React application. It includes basic setup and an example of using the `onMount` prop to select all elements on the canvas upon initialization.
```tsx
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function App() {
return (
{
editor.selectAll()
}}
/>
)
}
```
--------------------------------
### Access Editor via onMount Callback
Source: https://tldraw.dev/docs/editor
Use the `onMount` callback of the `Tldraw` component to get access to the editor instance.
```javascript
function App() {
return (
{
// your editor code here
}}
/>
)
}
```
--------------------------------
### Custom Preview Shape for Rich AI Output
Source: https://tldraw.dev/docs/ai
Implement a custom `ShapeUtil` to render rich AI output like live HTML previews or interactive prototypes. This example shows a basic `PreviewShapeUtil` using an `iframe` to display HTML content.
```typescript
// Abbreviated example—a full ShapeUtil also requires getDefaultProps, getGeometry, and indicator
class PreviewShapeUtil extends ShapeUtil {
static override type = 'preview' as const
component(shape: PreviewShape) {
return (
)
}
}
```
--------------------------------
### Node.js SQLiteSyncStorage with better-sqlite3
Source: https://tldraw.dev/docs/sync
Example of using SQLiteSyncStorage in a Node.js environment with the 'better-sqlite3' library for persistent tldraw document storage. It automatically manages database tables.
```typescript
import Database from 'better-sqlite3' // replace with 'node:sqlite' if using
import { SQLiteSyncStorage, NodeSqliteWrapper, TLSocketRoom } from '@tldraw/sync-core'
const db = new Database('rooms.db')
const sql = new NodeSqliteWrapper(db)
const storage = new SQLiteSyncStorage({ sql })
const room = new TLSocketRoom({ storage })
```
--------------------------------
### Create a Custom Stamp Tool
Source: https://tldraw.dev/docs/tools
Define a custom tool by extending StateNode and passing it to the Tldraw component's tools prop. This example shows a basic 'stamp' tool.
```typescript
import { StateNode, TLPointerEventInfo, Tldraw } from 'tldraw'
class StampTool extends StateNode {
static override id = 'stamp'
onPointerDown(info: TLPointerEventInfo) {
// Create a shape at the click position
}
}
export default function App() {
return
}
```
--------------------------------
### Defining a Basic Indicator Path
Source: https://tldraw.dev/docs/indicators
This example shows how to define a basic rectangular indicator path for a custom shape. The getIndicatorPath method returns a Path2D object representing the shape's bounds.
```typescript
import { HTMLContainer, Rectangle2d, ShapeUtil } from 'tldraw'
class CardShapeUtil extends ShapeUtil {
static override type = 'card'
getDefaultProps(): CardShape['props'] {
return { w: 100, h: 100 }
}
getGeometry(shape: CardShape) {
return new Rectangle2d({
width: shape.props.w,
height: shape.props.h,
isFilled: true,
})
}
component(shape: CardShape) {
return Hello
}
getIndicatorPath(shape: CardShape) {
const path = new Path2D()
path.rect(0, 0, shape.props.w, shape.props.h)
return path
}
}
```
--------------------------------
### Prompting the tldraw Agent
Source: https://tldraw.dev/docs/ai
Use the `useTldrawAgent` hook to get an agent instance. You can then prompt the agent with a simple message or a more detailed object including bounds for context.
```javascript
const agent = useTldrawAgent(editor)
// Simple prompt
agent.prompt('Draw a flowchart showing user authentication')
// With additional context
agent.prompt({
message: 'Add labels to these shapes',
bounds: { x: 0, y: 0, w: 500, h: 400 },
})
```
--------------------------------
### InMemorySyncStorage with onChange Callback
Source: https://tldraw.dev/docs/sync
Example of using InMemorySyncStorage for tldraw document state persistence. It keeps data in memory and requires an onChange callback to save snapshots to a database when changes occur.
```typescript
import { InMemorySyncStorage, TLSocketRoom } from '@tldraw/sync-core'
const storage = new InMemorySyncStorage({
snapshot: existingData, // optional: load from your database
onChange() {
// Save to your database when changes occur
saveToDatabase(storage.getSnapshot())
},
})
const room = new TLSocketRoom({ storage })
```
--------------------------------
### Custom Speech Bubble Shape with Draggable Tail Handle
Source: https://tldraw.dev/docs/handles
This example demonstrates creating a custom shape (speech bubble) with a draggable tail handle. It defines the shape's properties, geometry, handles, and drag behavior.
```typescript
import {
Polygon2d,
ShapeUtil,
TLHandle,
TLHandleDragInfo,
TLShape,
Vec,
ZERO_INDEX_KEY,
} from 'tldraw'
const SPEECH_BUBBLE_TYPE = 'speech-bubble'
declare module 'tldraw' {
export interface TLGlobalShapePropsMap {
[SPEECH_BUBBLE_TYPE]: { w: number; h: number; tailX: number; tailY: number }
}
}
type SpeechBubbleShape = TLShape
class SpeechBubbleUtil extends ShapeUtil {
static override type = SPEECH_BUBBLE_TYPE
getDefaultProps(): SpeechBubbleShape['props'] {
return { w: 200, h: 100, tailX: 100, tailY: 150 }
}
getGeometry(shape: SpeechBubbleShape) {
const { w, h, tailX, tailY } = shape.props
return new Polygon2d({
points: [
new Vec(0, 0),
new Vec(w, 0),
new Vec(w, h),
new Vec(w * 0.7, h),
new Vec(tailX, tailY),
new Vec(w * 0.3, h),
new Vec(0, h),
],
isFilled: true,
})
}
override getHandles(shape: SpeechBubbleShape): TLHandle[] {
return [
{
id: 'tail',
type: 'vertex',
label: 'Move tail',
index: ZERO_INDEX_KEY,
x: shape.props.tailX,
y: shape.props.tailY,
}
]
}
override onHandleDrag(shape: SpeechBubbleShape, { handle }: TLHandleDragInfo) {
return {
...shape,
props: {
...shape.props,
tailX: handle.x,
tailY: handle.y,
},
}
}
component(shape: SpeechBubbleShape) {
const geometry = this.getGeometry(shape)
return (
)
}
getIndicatorPath(shape: SpeechBubbleShape) {
const geometry = this.getGeometry(shape)
return new Path2D(geometry.getSvgPathData())
}
}
```
--------------------------------
### Query Last Created Shapes
Source: https://tldraw.dev/docs/driver
Retrieve shapes created by the driver's actions using query helpers. getLastCreatedShape() gets the most recent one, while getLastCreatedShapes(n) gets the last n.
```typescript
const shape = driver.getLastCreatedShape()
const lastFive = driver.getLastCreatedShapes(5)
```
--------------------------------
### Defining Complex Canvas Indicators
Source: https://tldraw.dev/docs/indicators
This example demonstrates how to return a TLIndicatorPath object for indicators requiring clipping or multiple paths. It includes a main path, a clip path, and additional paths for elements like arrowheads.
```typescript
override getIndicatorPath(shape: MyShape): TLIndicatorPath | undefined {
const bodyPath = new Path2D()
bodyPath.moveTo(0, 0)
bodyPath.lineTo(100, 100)
const arrowheadPath = new Path2D()
arrowheadPath.moveTo(90, 95)
arrowheadPath.lineTo(100, 100)
arrowheadPath.lineTo(95, 90)
const labelClipPath = new Path2D()
labelClipPath.rect(40, 40, 20, 20)
return {
path: bodyPath,
clipPath: labelClipPath, // Areas to exclude from the main path
additionalPaths: [arrowheadPath], // Extra paths to stroke
}
}
```
--------------------------------
### Get Shape Handles
Source: https://tldraw.dev/docs/handles
Use `Editor.getShapeHandles` to retrieve handles for a given shape. Returns `undefined` if the shape has no handles. Iterate through the handles to access their `id`, `x`, and `y` properties.
```javascript
const handles = editor.getShapeHandles(shape)
if (handles) {
for (const handle of handles) {
console.log(handle.id, handle.x, handle.y)
}
}
```
--------------------------------
### Change Active Tool
Source: https://tldraw.dev/docs/tools
Programmatically change the currently active tool in the tldraw editor using the editor.setCurrentTool method. Examples include switching to 'select', 'hand', or 'draw' tools.
```typescript
editor.setCurrentTool('select')
editor.setCurrentTool('hand')
editor.setCurrentTool('draw')
```
--------------------------------
### Use useSyncDemo Hook for Collaboration
Source: https://tldraw.dev/docs/collaboration
Connect to a hosted demo server for real-time collaboration using the useSyncDemo hook. Any app connecting to the same room ID will join the shared session.
```typescript
import { Tldraw } from 'tldraw'
import { useSyncDemo } from '@tldraw/sync'
function MyApp() {
const store = useSyncDemo({ roomId: 'my-unique-room-id' })
return
}
```
--------------------------------
### Saving and Loading Editor Snapshots
Source: https://tldraw.dev/docs/persistence
Implement custom storage backends by using `getSnapshot` to save the editor's state and `loadSnapshot` to restore it. The snapshot includes `document` (shapes, pages, bindings) for server storage and `session` (camera, selection, UI state) for local user data.
```javascript
import { getSnapshot, loadSnapshot } from 'tldraw'
// Save
const { document, session } = getSnapshot(editor.store)
await saveToDatabase(document)
// Load
const document = await loadFromDatabase()
loadSnapshot(editor.store, { document })
```
--------------------------------
### Initialize and Use Driver
Source: https://tldraw.dev/docs/driver
Wrap an Editor instance with a Driver and dispatch input events. Call dispose() when done.
```typescript
import { Driver } from '@tldraw/driver'
const driver = new Driver(editor)
driver.click(100, 200).pointerDown(300, 400).pointerMove(500, 600).pointerUp()
driver.keyPress('a')
driver.dispose()
```
--------------------------------
### Creating and Using a Standalone Store
Source: https://tldraw.dev/docs/persistence
Create a standalone tldraw store, load data into it using `loadSnapshot`, and pass it to the `Tldraw` component. This allows for custom persistence logic, such as using `localStorage` with throttled auto-save.
```jsx
import { useState } from 'react'
import { createTLStore, loadSnapshot, Tldraw } from 'tldraw'
export default function App() {
const [store] = useState(() => {
const store = createTLStore()
const saved = localStorage.getItem('my-drawing')
if (saved) {
loadSnapshot(store, JSON.parse(saved))
}
return store
})
return
}
```
--------------------------------
### Simple tldraw Sync Client Implementation
Source: https://tldraw.dev/docs/sync
A basic implementation of a tldraw sync client using the useSync hook. It connects to a specified WebSocket URI and utilizes a custom asset store for file uploads and resolution. Includes an external asset handler for URL-based bookmarks.
```typescript
import { Tldraw, TLAssetStore, Editor } from 'tldraw'
import { useSync } from '@tldraw/sync'
import { uploadFileAndReturnUrl } from './assets'
import { convertUrlToBookmarkAsset } from './unfurl'
function MyEditorComponent({ myRoomId }) {
// This hook creates a sync client that manages the websocket connection to the server
// and coordinates updates to the document state.
const store = useSync({
// This is how you tell the sync client which server and room to connect to.
uri: `wss://my-custom-backend.com/connect/${myRoomId}`,
// This is how you tell the sync client how to store and retrieve blobs.
assets: myAssetStore,
})
// When the tldraw Editor mounts, you can register an asset handler for the bookmark URLs.
return
}
const myAssetStore: TLAssetStore = {
upload(asset, file) {
return uploadFileAndReturnUrl(file)
},
resolve(asset) {
return asset.props.src
},
}
function registerUrlHandler(editor: Editor) {
editor.registerExternalAssetHandler('url', async ({ url }) => {
return await convertUrlToBookmarkAsset(url)
})
}
```
--------------------------------
### Multiplayer Synchronization with @tldraw/sync
Source: https://tldraw.dev/docs/persistence
Enable real-time collaboration by using the `@tldraw/sync` package. This allows multiple users to edit the same document simultaneously, see each other's cursors, and follow viewports.
```jsx
import { useSyncDemo } from '@tldraw/sync'
import { Tldraw } from 'tldraw'
export default function App() {
const store = useSyncDemo({ roomId: 'my-room-id' })
return
}
```
--------------------------------
### Batch Changes with Editor.run
Source: https://tldraw.dev/docs/editor
Use the `editor.run` method to group multiple editor changes into a single transaction. This improves performance and reduces overhead for history and distribution.
```javascript
editor.run(() => {
editor.createShapes(myShapes)
editor.sendToBack(myShapes)
editor.selectNone()
})
```
--------------------------------
### Server-side Schema Creation for Sync
Source: https://tldraw.dev/docs/sync
Create a schema using `createTLSchema` on the server to define custom shapes and bindings for synchronization. This schema is then passed to `TLSocketRoom` and includes shape properties and migrations.
```typescript
import { createTLSchema, defaultShapeSchemas, defaultBindingSchemas } from '@tldraw/tlschema'
import { TLSocketRoom } from '@tldraw/sync-core'
const schema = createTLSchema({
shapes: {
...defaultShapeSchemas,
myCustomShape: {
// Validations for this shapes `props`.
props: myCustomShapeProps,
// Migrations between versions of this shape.
migrations: myCustomShapeMigrations,
},
// The schema knows about this shape, but it has no migrations or validation.
mySimpleShape: {},
},
bindings: defaultBindingSchemas,
})
// Later, in your app server:
const room = new TLSocketRoom({
schema: schema,
// ...
})
```
--------------------------------
### Migrate Data with SQLiteSyncStorage
Source: https://tldraw.dev/docs/sync
Use this when migrating data to SQLiteSyncStorage. It checks for existing data, attempts to load from a legacy system, and creates a new room if no data is found. Requires `better-sqlite3` and `@tldraw/sync-core`.
```typescript
import { SQLiteSyncStorage, NodeSqliteWrapper, TLSocketRoom } from '@tldraw/sync-core'
import Database from 'better-sqlite3'
function loadOrMakeRoom(roomId: string, db: Database.Database) {
const sql = new NodeSqliteWrapper(db, { tablePrefix: `room_${roomId}_` })
// Check if we already have data in SQLite
if (SQLiteSyncStorage.hasBeenInitialized(sql)) {
const storage = new SQLiteSyncStorage({ sql })
return new TLSocketRoom({ storage })
}
// Try to load from legacy system
const legacyData = loadRoomDataFromLegacyStore(roomId)
if (legacyData) {
const snapshot = convertOldDataToSnapshot(legacyData)
const storage = new SQLiteSyncStorage({ sql, snapshot })
deleteLegacyRoomData(roomId)
return new TLSocketRoom({ storage })
}
// No data - create a new empty room
return new TLSocketRoom({ storage: new SQLiteSyncStorage({ sql }) })
}
```
--------------------------------
### Agent Action Utility for Creating Shapes
Source: https://tldraw.dev/docs/ai
This illustrates a simplified `CreateActionUtil` class that agents can use to create shapes on the canvas. It handles sanitizing input and applying the action to the editor.
```typescript
// Simplified illustration—the actual implementation converts through a SimpleShape
// intermediate format. See CreateActionUtil in the agent starter kit for the full code.
class CreateActionUtil extends AgentActionUtil {
override applyAction(action: Streaming, helpers: AgentHelpers) {
if (!action.complete) return
const position = helpers.removeOffsetFromVec({ x: action.x, y: action.y })
this.editor.createShape({
type: action.shapeType,
x: position.x,
y: position.y,
props: action.props,
})
}
}
```
--------------------------------
### Migrate Data with InMemorySyncStorage
Source: https://tldraw.dev/docs/sync
Use this when migrating data to InMemorySyncStorage. It loads existing data, attempts to convert and save legacy data, and creates a new room if no data is found. Requires `@tldraw/sync-core`.
```typescript
import { InMemorySyncStorage, TLSocketRoom } from '@tldraw/sync-core'
async function loadOrMakeRoom(roomId: string) {
const data = await loadRoomDataFromCurrentStore(roomId)
if (data) {
const storage = new InMemorySyncStorage({ snapshot: data })
return new TLSocketRoom({ storage })
}
const legacyData = await loadRoomDataFromLegacyStore(roomId)
if (legacyData) {
// Convert your old data to a TLStoreSnapshot.
const snapshot = convertOldDataToSnapshot(legacyData)
// Load it into the room.
const storage = new InMemorySyncStorage({ snapshot })
const room = new TLSocketRoom({ storage })
// Save an updated copy of the snapshot in the new place
// so that next time we can load it directly.
await saveRoomData(roomId, storage.getSnapshot())
// Optionally delete the old data.
await deleteLegacyRoomData(roomId)
// And finally return the room.
return room
}
// If there's no data at all, just make a new blank room.
return new TLSocketRoom({ storage: new InMemorySyncStorage() })
}
```
--------------------------------
### Update wrangler.toml for SQLite Migration
Source: https://tldraw.dev/docs/sync
Add new migrations for SQLite-backed Durable Objects and update bindings to point to the new class. Keep the old class for a transition period.
```toml
# Keep existing migration for old class
[[migrations]]
tag = "v1"
new_classes = ["TldrawDurableObject"]
# Add new migration for SQLite-backed class
[[migrations]]
tag = "v2"
new_sqlite_classes = ["TldrawDurableObjectSqlite"]
[durable_objects]
bindings = [
# Point to the new SQLite-backed class
{ name = "TLDRAW_DURABLE_OBJECT", class_name = "TldrawDurableObjectSqlite" },
]
```
--------------------------------
### Automatic Local Persistence with persistenceKey
Source: https://tldraw.dev/docs/persistence
Use the `persistenceKey` prop for simple, automatic saving to IndexedDB and synchronization across browser tabs. Each unique key represents a distinct document.
```jsx
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function App() {
return (
)
}
```
--------------------------------
### Track Reactive State with useEditor and track
Source: https://tldraw.dev/docs/editor
Use the `track` wrapper with the `useEditor` hook to create reactive components that automatically update when editor state changes, such as the number of selected shapes.
```javascript
import { track, useEditor } from 'tldraw'
export const SelectedShapeIdsCount = track(() => {
const editor = useEditor()
return
{editor.getSelectedShapeIds().length}
})
```
--------------------------------
### Simulate Drawing with Pointer Events
Source: https://tldraw.dev/docs/driver
Simulate a drawing action by chaining pointer down, move, and up events. Pointer coordinates are in screen space.
```typescript
editor.setCurrentTool('draw')
driver.pointerDown(100, 100)
driver.pointerMove(150, 120)
driver.pointerMove(200, 140)
driver.pointerUp()
```
--------------------------------
### Snap Handle to Point
Source: https://tldraw.dev/docs/handles
Configure a handle to snap to points on other shapes. This is useful for precise placement of shape vertices.
```typescript
{
id: 'end',
type: 'vertex',
index: ZERO_INDEX_KEY,
x: shape.props.endX,
y: shape.props.endY,
snapType: 'point', // Snap to points on other shapes
}
```
--------------------------------
### Client-side Sync Configuration with Custom Shapes and Bindings
Source: https://tldraw.dev/docs/sync
Configure the `useSync` hook on the client to include custom shapes and bindings alongside default ones. This ensures that custom elements are recognized and synchronized correctly.
```typescript
import { Tldraw, defaultShapeUtils, defaultBindingUtils } from 'tldraw'
import { useSync } from '@tldraw/sync'
function MyApp() {
const store = useSync({
uri: '...',
assets: myAssetStore,
shapeUtils: useMemo(() => [...customShapeUtils, ...defaultShapeUtils], []),
bindingUtils: useMemo(() => [...customBindingUtils, ...defaultBindingUtils], []),
})
return
}
```
--------------------------------
### Simulate Keyboard Input with Modifiers
Source: https://tldraw.dev/docs/driver
Simulate keyboard input, including holding modifier keys like Shift. The driver tracks modifier state automatically.
```typescript
driver.keyDown('Shift')
driver.click(100, 100, { target: 'canvas' })
driver.keyUp('Shift')
```
--------------------------------
### Create Embed Shape for AI Content
Source: https://tldraw.dev/docs/ai
Use the `EmbedShapeUtil` to display AI-generated websites on the canvas. Specify the URL and dimensions for the embedded content.
```typescript
editor.createShape({
type: 'embed',
x: 100,
y: 100,
props: {
url: 'https://generated-preview.example.com/abc123',
w: 800,
h: 600,
},
})
```
--------------------------------
### Create a Custom ShapeUtil in tldraw
Source: https://tldraw.dev/docs/shapes
Implement the required methods for a custom shape's behavior, including default properties, geometry for hit testing, rendering component, and indicator path.
```typescript
import { HTMLContainer, Rectangle2d, ShapeUtil } from 'tldraw'
class CardShapeUtil extends ShapeUtil {
static override type = CARD_TYPE
getDefaultProps(): CardShape['props'] {
return { w: 100, h: 100 }
}
getGeometry(shape: CardShape) {
return new Rectangle2d({
width: shape.props.w,
height: shape.props.h,
isFilled: true,
})
}
component(shape: CardShape) {
return Hello
}
getIndicatorPath(shape: CardShape) {
const path = new Path2D()
path.rect(0, 0, shape.props.w, shape.props.h)
return path
}
}
```
--------------------------------
### Export Both Durable Object Classes
Source: https://tldraw.dev/docs/sync
Ensure both the old stub class and the new SQLite-backed class are exported from the worker entry point to facilitate the migration.
```typescript
export { TldrawDurableObject, TldrawDurableObjectSqlite } from './TldrawDurableObject'
```
--------------------------------
### Create a Mermaid Diagram
Source: https://tldraw.dev/docs/mermaid
Call `createMermaidDiagram` with an `Editor` instance and a Mermaid source string to render a diagram on the canvas. By default, the diagram is centered on the current viewport.
```typescript
import { createMermaidDiagram } from '@tldraw/mermaid'
await createMermaidDiagram(
editor,
`
flowchart TD
A[Start] --> B{Decision}
B -->|Yes| C[Do something]
B -->|No| D[Do something else]
`
)
```
--------------------------------
### Handle Unsupported Mermaid Diagrams with SVG Fallback
Source: https://tldraw.dev/docs/mermaid
Provide an `onUnsupportedDiagram` callback to `createMermaidDiagram` to handle unsupported diagram types or parse failures by falling back to importing Mermaid's rendered SVG. Without this callback, an error is thrown.
```typescript
await createMermaidDiagram(editor, text, {
onUnsupportedDiagram(svgString) {
editor.putExternalContent({ type: 'svg-text', text: svgString })
}
})
```
--------------------------------
### Create Image Shape from AI-Generated Asset
Source: https://tldraw.dev/docs/ai
For AI-generated images, first create an asset using `AssetRecordType` from a blob or URL, then create an image shape on the canvas referencing the asset's ID. Ensure `mimeType`, `w`, and `h` are correctly set.
```typescript
const asset = AssetRecordType.create({
id: AssetRecordType.createId(),
type: 'image',
props: {
src: generatedImageUrl,
w: 512,
h: 512,
mimeType: 'image/png',
name: 'generated-image.png',
isAnimated: false,
},
})
editor.createAssets([asset])
editor.createShape({
type: 'image',
x: 100,
y: 100,
props: {
assetId: asset.id,
w: 512,
h: 512,
},
})
```
--------------------------------
### Implement SQLite Fallback Loading in Durable Object
Source: https://tldraw.dev/docs/sync
Rename the Durable Object class and update the loading logic to prioritize SQLite storage, falling back to R2 for legacy data. Includes a stub for the old class name.
```typescript
import {
DurableObjectSqliteSyncWrapper,
RoomSnapshot,
SQLiteSyncStorage,
TLSocketRoom,
} from '@tldraw/sync-core'
import { TLRecord, createTLSchema, defaultShapeSchemas } from '@tldraw/tlschema'
import { AutoRouter, error, IRequest } from 'itty-router'
// Empty stub for the old class name - required until migration is complete
export class TldrawDurableObject {}
const schema = createTLSchema({ shapes: defaultShapeSchemas })
// Renamed from TldrawDurableObject
export class TldrawDurableObjectSqlite {
private roomPromise: Promise> | null = null
private roomId: string | null = null
constructor(
private ctx: DurableObjectState,
private env: Env
) {
// Load roomId from DO storage (needed for R2 fallback)
ctx.blockConcurrencyWhile(async () => {
this.roomId = (await ctx.storage.get('roomId')) as string | null
})
}
// ... handleConnect, router, etc. stay the same ...
private async loadRoom(): Promise> {
const sql = new DurableObjectSqliteSyncWrapper(this.ctx.storage)
// If SQLite already has data, use it directly
if (SQLiteSyncStorage.hasBeenInitialized(sql)) {
const storage = new SQLiteSyncStorage({ sql })
return new TLSocketRoom({ schema, storage })
}
// Try to load from R2 (legacy storage, only happens once per room)
if (this.roomId) {
const r2Object = await this.env.TLDRAW_BUCKET.get(`rooms/${this.roomId}`)
if (r2Object) {
const snapshot = (await r2Object.json()) as RoomSnapshot
const storage = new SQLiteSyncStorage({ sql, snapshot })
// Optionally delete the R2 object after successful migration
// await this.env.TLDRAW_BUCKET.delete(`rooms/${this.roomId}`)
return new TLSocketRoom({ schema, storage })
}
}
// No existing data - create fresh room
const storage = new SQLiteSyncStorage({ sql })
return new TLSocketRoom({ schema, storage })
}
}
```
--------------------------------
### Cloudflare Durable Object for SQLiteSyncStorage
Source: https://tldraw.dev/docs/sync
Implementation of a tldraw sync backend using SQLiteSyncStorage within a Cloudflare Durable Object. This approach ensures data persistence across process restarts.
```typescript
import { DurableObject } from 'cloudflare:workers'
import { SQLiteSyncStorage, DurableObjectSqliteSyncWrapper, TLSocketRoom } from '@tldraw/sync-core'
export class TLSyncDurableObject extends DurableObject {
private room: TLSocketRoom
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env)
const sql = new DurableObjectSqliteSyncWrapper(ctx.storage)
const storage = new SQLiteSyncStorage({ sql })
this.room = new TLSocketRoom({ storage })
}
}
```
--------------------------------
### Export Canvas as SVG or Image
Source: https://tldraw.dev/docs/ai
Use `Editor.getSvgString` to export the current view as an SVG string, or `Editor.toImage` to export it as a blob (e.g., PNG). These methods are useful for sending visual representations of the canvas to AI models.
```javascript
const svg = await editor.getSvgString(editor.getCurrentPageShapes())
const { blob } = await editor.toImage(editor.getCurrentPageShapes(), { format: 'png' })
```
--------------------------------
### Register External Content Handler for Mermaid Pasting
Source: https://tldraw.dev/docs/mermaid
Register an external content handler to detect and convert pasted Mermaid text into tldraw shapes. This handler uses a regex to identify Mermaid syntax and dynamically imports the `@tldraw/mermaid` package to avoid a large initial bundle size.
```typescript
import { useEffect } from 'react'
import { defaultHandleExternalTextContent, useEditor } from 'tldraw'
const MERMAID_KEYWORD =
/^
(flowchart|graph|sequenceDiagram|stateDiagram|classDiagram|erDiagram|gantt|pie|gitGraph|mindmap)/
export function MermaidPasteHandler() {
const editor = useEditor()
useEffect(() => {
editor.registerExternalContentHandler('text', async (content) => {
if (!MERMAID_KEYWORD.test(content.text)) {
await defaultHandleExternalTextContent(editor, content)
return
}
try {
const { createMermaidDiagram } = await import('@tldraw/mermaid')
await createMermaidDiagram(editor, content.text, {
async onUnsupportedDiagram(svgString) {
await editor.putExternalContent({ type: 'svg-text', text: svgString })
},
})
} catch {
await defaultHandleExternalTextContent(editor, content)
}
})
}, [editor])
return null
}
```
--------------------------------
### Register an Unfurling Service for Bookmark Shapes
Source: https://tldraw.dev/docs/sync
Register a custom unfurling service with the tldraw editor to enable the built-in bookmark shape functionality. This handler is invoked when a URL is added to the canvas.
```jsx
{
editor.registerExternalAssetHandler('url', unfurlBookmarkUrl)
}}
/>
```
--------------------------------
### Implement WebSocket Hibernation with Cloudflare Durable Objects
Source: https://tldraw.dev/docs/sync
Use this pattern in a Cloudflare Durable Object to manage WebSocket connections across hibernation. It persists session state to WebSocket attachments and resumes sessions upon waking.
```typescript
import {
DurableObjectSqliteSyncWrapper,
type SessionStateSnapshot,
SQLiteSyncStorage,
TLSocketRoom,
} from '@tldraw/sync-core'
import { TLRecord } from '@tldraw/tlschema'
import { DurableObject } from 'cloudflare:workers'
interface SocketAttachment {
sessionId: string
snapshot?: SessionStateSnapshot
}
export class TldrawDurableObject extends DurableObject {
private room: TLSocketRoom | null = null
private getOrCreateRoom() {
if (this.room) return this.room
const sql = new DurableObjectSqliteSyncWrapper(this.ctx.storage)
const storage = new SQLiteSyncStorage({ sql })
this.room = new TLSocketRoom({
storage,
// Cloudflare keeps WebSockets alive across hibernation, so let it manage timeouts.
clientTimeout: Infinity,
// Persist each session's snapshot to its WebSocket attachment when it goes idle.
onSessionSnapshot: (sessionId, snapshot) => {
const ws = this.sessionIdToWs.get(sessionId)
if (ws) ws.serializeAttachment({ sessionId, snapshot })
},
})
// Resume any sessions whose sockets survived hibernation.
for (const ws of this.ctx.getWebSockets()) {
const attachment = ws.deserializeAttachment() as SocketAttachment | null
if (attachment?.snapshot) {
this.room.handleSocketResume({
sessionId: attachment.sessionId,
socket: ws,
snapshot: attachment.snapshot,
})
}
}
return this.room
}
}
```
--------------------------------
### Batch Changes Ignoring History
Source: https://tldraw.dev/docs/editor
Pass an options object to `editor.run` to control behavior, such as ignoring undo/redo history for a batch of changes.
```javascript
// Make changes without affecting undo/redo history
editor.run(
() => {
editor.createShapes(myShapes)
},
{ history: 'ignore' }
)
```
--------------------------------
### Clipboard Operations
Source: https://tldraw.dev/docs/driver
Perform copy and paste actions using the driver's in-memory clipboard, which is separate from the system clipboard.
```typescript
driver.copy() // copies the current selection
driver.paste({ x: 400, y: 400 })
```
--------------------------------
### Defining a Simple Rectangular Indicator
Source: https://tldraw.dev/docs/indicators
A straightforward implementation of getIndicatorPath returning a Path2D for a rectangle. This is suitable for basic shapes.
```typescript
class MyShapeUtil extends ShapeUtil {
override getIndicatorPath(shape: MyShape): Path2D | undefined {
const path = new Path2D()
path.rect(0, 0, shape.props.w, shape.props.h)
return path
}
}
```
--------------------------------
### Customize Mermaid Node Rendering
Source: https://tldraw.dev/docs/mermaid
Customize how Mermaid nodes are rendered by providing a `mapNodeToRenderSpec` function within the `blueprintRender` options of `createMermaidDiagram`. This allows mapping Mermaid nodes to custom tldraw shape types or specific variants.
```typescript
await createMermaidDiagram(editor, text, {
blueprintRender: {
mapNodeToRenderSpec(input) {
if (input.diagramKind === 'mindmap') {
return { variant: 'shape', type: 'note', props: {} }
}
// Return undefined to keep the package default for this node.
return undefined
}
},
})
```
--------------------------------
### Manipulate Selection with Page Coordinates
Source: https://tldraw.dev/docs/driver
Transform the current selection using page coordinates. The driver handles internal conversion to screen space for these operations.
```typescript
driver.translateSelection(50, 0)
driver.rotateSelection(Math.PI / 4)
driver.resizeSelection({ scaleX: 2 }, 'bottom_right')
```
--------------------------------
### Extract Structured Text Content
Source: https://tldraw.dev/docs/ai
Access shape data directly from the store to extract text content from 'text', 'note', or 'geo' shapes. This provides structured data for AI analysis, including shape IDs, types, text content, and bounding boxes.
```javascript
const shapes = editor.getCurrentPageShapes()
const textContent = shapes
.filter((s) => s.type === 'text' || s.type === 'note' || s.type === 'geo')
.map((s) => ({
id: s.id,
type: s.type,
text: s.props.text,
bounds: editor.getShapePageBounds(s),
}))
```
--------------------------------
### Access Editor via useEditor Hook
Source: https://tldraw.dev/docs/editor
The `useEditor` hook returns the editor instance and must be called within the JSX of the `Tldraw` component.
```javascript
function InsideOfContext() {
const editor = useEditor()
// your editor code here
return null
}
function App() {
return (
)
}
```
--------------------------------
### Custom Handle Snap Geometry
Source: https://tldraw.dev/docs/handles
Override the default handle snapping behavior by providing custom snap points and geometry. This allows for fine-grained control over what handles snap to, including self-snapping behavior.
```typescript
import { HandleSnapGeometry, ShapeUtil } from 'tldraw'
class BezierCurveUtil extends ShapeUtil {
// ...
override getHandleSnapGeometry(shape: BezierCurveShape): HandleSnapGeometry {
return {
// Points other shapes' handles can snap to
points: [shape.props.start, shape.props.end],
// Points this shape's own handles can snap to (for self-snapping)
getSelfSnapPoints: (handle) => {
if (handle.id === 'controlPoint') {
return [shape.props.start, shape.props.end]
}
return []
},
}
}
}
```
--------------------------------
### Respond to Handle Drags
Source: https://tldraw.dev/docs/handles
Implement `onHandleDrag` in your `ShapeUtil` to update shape properties when a user drags a handle. The `handle` object contains the updated local coordinates.
```typescript
import { ShapeUtil, TLHandleDragInfo } from 'tldraw'
class SpeechBubbleUtil extends ShapeUtil {
// ...
override onHandleDrag(shape: SpeechBubbleShape, { handle }: TLHandleDragInfo) {
return {
...shape,
props: {
...shape.props,
tailX: handle.x,
tailY: handle.y,
},
}
}
}
```
--------------------------------
### Define LLM Node for Workflows
Source: https://tldraw.dev/docs/ai
Illustrative pseudocode for defining a custom node type that calls an AI model. This node processes inputs and returns outputs, suitable for integration into a visual workflow system.
```typescript
class LLMNode extends NodeDefinition {
static type = 'llm'
await execute(shape, node, inputs) {
const response = await fetch('/api/generate', {
method: 'POST',
body: JSON.stringify({ prompt: inputs.prompt })
})
const result = await response.json()
return { output: result.text }
}
}
```
--------------------------------
### Define Handles for a Custom Shape
Source: https://tldraw.dev/docs/handles
Implement `getHandles` in your `ShapeUtil` to define interactive control points for a shape. Handle coordinates are relative to the shape's local coordinate system.
```typescript
import { ShapeUtil, TLHandle, ZERO_INDEX_KEY } from 'tldraw'
class MyShapeUtil extends ShapeUtil {
// ...
override getHandles(shape: MyShape): TLHandle[] {
return [
{
id: 'point',
type: 'vertex',
index: ZERO_INDEX_KEY,
x: shape.props.pointX,
y: shape.props.pointY,
},
]
}
}
```
--------------------------------
### Batch Changes on Locked Shapes
Source: https://tldraw.dev/docs/editor
Use the `ignoreShapeLock` option within `editor.run` to make changes to locked shapes.
```javascript
// Make changes to locked shapes
editor.run(
() => {
editor.updateShapes(myLockedShapes)
},
{ ignoreShapeLock: true }
)
```
--------------------------------
### Register and Use Custom Shape in tldraw
Source: https://tldraw.dev/docs/shapes
Pass your custom ShapeUtil to the Tldraw component and create an instance of your shape when the editor mounts.
```typescript
export default function () {
return (
{
editor.createShape({ type: 'card' })
}}
/>
)
}
```
--------------------------------
### Snap Handle with Angle Reference
Source: https://tldraw.dev/docs/handles
Enable angle snapping for a handle relative to another specified handle. This is particularly useful for bezier curves to maintain angular relationships between control points and endpoints.
```typescript
{
id: 'controlPoint',
type: 'vertex',
index: indices[1],
x: shape.props.cpX,
y: shape.props.cpY,
snapType: 'align',
snapReferenceHandleId: 'start', // Angle snaps relative to 'start' handle
}
```
--------------------------------
### Define Custom Shape Type with TypeScript
Source: https://tldraw.dev/docs/shapes
Register your custom shape's properties using TypeScript module augmentation. This ensures type safety for your shape's data.
```typescript
const CARD_TYPE = 'card'
declare module 'tldraw' {
export interface TLGlobalShapePropsMap {
[CARD_TYPE]: { w: number; h: number }
}
}
type CardShape = TLShape
```
=== COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.