tldraw zero - package shuffle (#1710)

This PR moves code between our packages so that:
- @tldraw/editor is a “core” library with the engine and canvas but no
shapes, tools, or other things
- @tldraw/tldraw contains everything particular to the experience we’ve
built for tldraw

At first look, this might seem like a step away from customization and
configuration, however I believe it greatly increases the configuration
potential of the @tldraw/editor while also providing a more accurate
reflection of what configuration options actually exist for
@tldraw/tldraw.

## Library changes

@tldraw/editor re-exports its dependencies and @tldraw/tldraw re-exports
@tldraw/editor.

- users of @tldraw/editor WITHOUT @tldraw/tldraw should almost always
only import things from @tldraw/editor.
- users of @tldraw/tldraw should almost always only import things from
@tldraw/tldraw.

- @tldraw/polyfills is merged into @tldraw/editor
- @tldraw/indices is merged into @tldraw/editor
- @tldraw/primitives is merged mostly into @tldraw/editor, partially
into @tldraw/tldraw
- @tldraw/file-format is merged into @tldraw/tldraw
- @tldraw/ui is merged into @tldraw/tldraw

Many (many) utils and other code is moved from the editor to tldraw. For
example, embeds now are entirely an feature of @tldraw/tldraw. The only
big chunk of code left in core is related to arrow handling.

## API Changes

The editor can now be used without tldraw's assets. We load them in
@tldraw/tldraw instead, so feel free to use whatever fonts or images or
whatever that you like with the editor.

All tools and shapes (except for the `Group` shape) are moved to
@tldraw/tldraw. This includes the `select` tool.

You should use the editor with at least one tool, however, so you now
also need to send in an `initialState` prop to the Editor /
<TldrawEditor> component indicating which state the editor should begin
in.

The `components` prop now also accepts `SelectionForeground`.

The complex selection component that we use for tldraw is moved to
@tldraw/tldraw. The default component is quite basic but can easily be
replaced via the `components` prop. We pass down our tldraw-flavored
SelectionFg via `components`.

Likewise with the `Scribble` component: the `DefaultScribble` no longer
uses our freehand tech and is a simple path instead. We pass down the
tldraw-flavored scribble via `components`.

The `ExternalContentManager` (`Editor.externalContentManager`) is
removed and replaced with a mapping of types to handlers.

- Register new content handlers with
`Editor.registerExternalContentHandler`.
- Register new asset creation handlers (for files and URLs) with
`Editor.registerExternalAssetHandler`

### Change Type

- [x] `major` — Breaking change

### Test Plan

- [x] Unit Tests
- [x] End to end tests

### Release Notes

- [@tldraw/editor] lots, wip
- [@tldraw/ui] gone, merged to tldraw/tldraw
- [@tldraw/polyfills] gone, merged to tldraw/editor
- [@tldraw/primitives] gone, merged to tldraw/editor / tldraw/tldraw
- [@tldraw/indices] gone, merged to tldraw/editor
- [@tldraw/file-format] gone, merged to tldraw/tldraw

---------

Co-authored-by: alex <alex@dytry.ch>
This commit is contained in:
Steve Ruiz 2023-07-17 22:22:34 +01:00 committed by GitHub
parent 43a0dd83f8
commit b7d9c8684c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
618 changed files with 8939 additions and 11666 deletions

View file

@ -76,5 +76,11 @@ module.exports = {
'import/no-internal-modules': 'off', 'import/no-internal-modules': 'off',
}, },
}, },
// {
// files: ['packages/tldraw/src/test/**/*'],
// rules: {
// 'import/no-internal-modules': 'off',
// },
// },
], ],
} }

View file

@ -82,15 +82,10 @@ This repository's contents is divided across four primary sections:
- `assets`: a library for working with tldraw's fonts and translations - `assets`: a library for working with tldraw's fonts and translations
- `editor`: the tldraw editor - `editor`: the tldraw editor
- `file-format`: a library for working with tldraw's `.tldr` file format
- `indices`: a library for working with tldraw's indices
- `polyfills`: a collection of polyfills used by tldraw
- `primitives`: low-level primitives for working with vectors and geometry
- `state`: a signals library, also known as signia - `state`: a signals library, also known as signia
- `store`: an in-memory reactive database - `store`: an in-memory reactive database
- `tldraw`: the main tldraw package containing both the editor and the UI - `tldraw`: the main tldraw package containing both the editor and the UI
- `tlschema`: shape definitions and migrations - `tlschema`: shape definitions and migrations
- `ui`: the editor's user interface
- `utils`: low-level data utilities shared by other libraries - `utils`: low-level data utilities shared by other libraries
- `validate`: a validation library used for run-time validation - `validate`: a validation library used for run-time validation

View file

@ -36,7 +36,7 @@ export async function setupPage(page: PlaywrightTestArgs['page']) {
await page.goto('http://localhost:5420/end-to-end') await page.goto('http://localhost:5420/end-to-end')
await page.waitForSelector('.tl-canvas') await page.waitForSelector('.tl-canvas')
await page.evaluate(() => { await page.evaluate(() => {
editor.setAnimationSpeed(0) editor.animationSpeed = 0
}) })
} }

View file

@ -1,20 +1,11 @@
import test, { Page, expect } from '@playwright/test' import test, { expect } from '@playwright/test'
import { Editor, TLShapeId, TLShapePartial } from '@tldraw/tldraw' import { Editor, TLShapeId, TLShapePartial } from '@tldraw/tldraw'
import { assert } from '@tldraw/utils'
import { rename, writeFile } from 'fs/promises' import { rename, writeFile } from 'fs/promises'
import { setupPage } from '../shared-e2e' import { setupPage } from '../shared-e2e'
let page: Page
declare const editor: Editor declare const editor: Editor
test.describe('Export snapshots', () => { test.describe('Export snapshots', () => {
test.beforeAll(async ({ browser }) => {
page = await browser.newPage()
})
test.beforeEach(async () => {
await setupPage(page)
})
const snapshots = {} as Record<string, TLShapePartial[]> const snapshots = {} as Record<string, TLShapePartial[]>
for (const fill of ['none', 'semi', 'solid', 'pattern']) { for (const fill of ['none', 'semi', 'solid', 'pattern']) {
@ -172,7 +163,9 @@ test.describe('Export snapshots', () => {
} }
for (const [name, shapes] of Object.entries(snapshots)) { for (const [name, shapes] of Object.entries(snapshots)) {
test(`Exports with ${name}`, async () => { test(`Exports with ${name}`, async ({ browser }) => {
const page = await browser.newPage()
await setupPage(page)
await page.evaluate((shapes) => { await page.evaluate((shapes) => {
editor editor
.updateInstanceState({ exportBackground: false }) .updateInstanceState({ exportBackground: false })
@ -188,17 +181,17 @@ test.describe('Export snapshots', () => {
await page.click('[data-testid="menu-item.export-as-svg"]') await page.click('[data-testid="menu-item.export-as-svg"]')
const download = await downloadEvent const download = await downloadEvent
const path = await download.path() const path = (await download.path()) as string
assert(path) // assert(path)
await rename(path, path + '.svg') await rename(path, path + '.svg')
await writeFile( await writeFile(
path + '.html', path + '.html',
` `
<!DOCTYPE html> <!DOCTYPE html>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<img src="${path}.svg" /> <img src="${path}.svg" />
`, `,
'utf-8' 'utf-8'
) )

View file

@ -327,3 +327,32 @@ test.describe('Keyboard Shortcuts', () => {
}) })
}) })
}) })
test.describe('Delete bug', () => {
test.beforeAll(async ({ browser }) => {
page = await browser.newPage()
await setupPage(page)
})
test('delete bug without drag', async () => {
await page.keyboard.press('r')
await page.mouse.click(100, 100)
await page.keyboard.press('Backspace')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'delete-shapes',
data: { source: 'kbd' },
})
})
test('delete bug with drag', async () => {
await page.keyboard.press('r')
await page.mouse.move(100, 100)
await page.mouse.down()
await page.mouse.up()
await page.keyboard.press('Backspace')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'delete-shapes',
data: { source: 'kbd' },
})
})
})

View file

@ -37,10 +37,7 @@
"@babel/plugin-proposal-decorators": "^7.21.0", "@babel/plugin-proposal-decorators": "^7.21.0",
"@playwright/test": "^1.35.1", "@playwright/test": "^1.35.1",
"@tldraw/assets": "workspace:*", "@tldraw/assets": "workspace:*",
"@tldraw/state": "workspace:*",
"@tldraw/tldraw": "workspace:*", "@tldraw/tldraw": "workspace:*",
"@tldraw/utils": "workspace:*",
"@tldraw/validate": "workspace:*",
"@vercel/analytics": "^1.0.1", "@vercel/analytics": "^1.0.1",
"lazyrepo": "0.0.0-alpha.27", "lazyrepo": "0.0.0-alpha.27",
"react": "^18.2.0", "react": "^18.2.0",

View file

@ -2,24 +2,30 @@ import { Tldraw, TLEditorComponents } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css' import '@tldraw/tldraw/tldraw.css'
const components: Partial<TLEditorComponents> = { const components: Partial<TLEditorComponents> = {
Brush: ({ brush }) => ( Brush: function MyBrush({ brush }) {
<rect return (
className="tl-brush" <svg className="tl-overlays__item">
stroke="red" <rect
fill="none" className="tl-brush"
width={Math.max(1, brush.w)} stroke="red"
height={Math.max(1, brush.h)} fill="none"
transform={`translate(${brush.x},${brush.y})`} width={Math.max(1, brush.w)}
/> height={Math.max(1, brush.h)}
), transform={`translate(${brush.x},${brush.y})`}
/>
</svg>
)
},
Scribble: ({ scribble, opacity, color }) => { Scribble: ({ scribble, opacity, color }) => {
return ( return (
<polyline <svg className="tl-overlays__item">
points={scribble.points.map((p) => `${p.x},${p.y}`).join(' ')} <polyline
stroke={color ?? 'black'} points={scribble.points.map((p) => `${p.x},${p.y}`).join(' ')}
opacity={opacity ?? '1'} stroke={color ?? 'black'}
fill="none" opacity={opacity ?? '1'}
/> fill="none"
/>
</svg>
) )
}, },
SnapLine: null, SnapLine: null,

View file

@ -8,6 +8,12 @@ const MOVING_CURSOR_SPEED = 0.25 // 0 is stopped, 1 is full send
const MOVING_CURSOR_RADIUS = 100 const MOVING_CURSOR_RADIUS = 100
const CURSOR_CHAT_MESSAGE = 'Hey, I think this is just great.' const CURSOR_CHAT_MESSAGE = 'Hey, I think this is just great.'
// Note:
// Almost all of the information below is calculated automatically by helpers in the editor.
// For a more realistic implementation, see the yjs example in this examples folder. If anything,
// this example should be used to understand the data model and test designs, not as a reference
// for how to implement user presence.
export default function UserPresenceExample() { export default function UserPresenceExample() {
const rRaf = useRef<any>(-1) const rRaf = useRef<any>(-1)
return ( return (

View file

@ -1,12 +1,11 @@
import { Tldraw, createTLStore, defaultShapes } from '@tldraw/tldraw' import { Tldraw, createTLStore, defaultShapeUtils, throttle } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css' import '@tldraw/tldraw/tldraw.css'
import { throttle } from '@tldraw/utils'
import { useLayoutEffect, useState } from 'react' import { useLayoutEffect, useState } from 'react'
const PERSISTENCE_KEY = 'example-3' const PERSISTENCE_KEY = 'example-3'
export default function PersistenceExample() { export default function PersistenceExample() {
const [store] = useState(() => createTLStore({ shapes: defaultShapes })) const [store] = useState(() => createTLStore({ shapeUtils: defaultShapeUtils }))
const [loadingState, setLoadingState] = useState< const [loadingState, setLoadingState] = useState<
{ status: 'loading' } | { status: 'ready' } | { status: 'error'; error: string } { status: 'loading' } | { status: 'ready' } | { status: 'error'; error: string }
>({ >({

View file

@ -4,12 +4,11 @@ import {
DefaultColorStyle, DefaultColorStyle,
HTMLContainer, HTMLContainer,
StyleProp, StyleProp,
T,
TLBaseShape, TLBaseShape,
TLDefaultColorStyle, TLDefaultColorStyle,
defineShape,
getDefaultColorTheme, getDefaultColorTheme,
} from '@tldraw/tldraw' } from '@tldraw/tldraw'
import { T } from '@tldraw/validate'
// Define a style that can be used across multiple shapes. // Define a style that can be used across multiple shapes.
// The ID (myApp:filter) must be globally unique, so we recommend prefixing it with a namespace. // The ID (myApp:filter) must be globally unique, so we recommend prefixing it with a namespace.
@ -33,6 +32,15 @@ export type CardShape = TLBaseShape<
export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> { export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
static override type = 'card' as const static override type = 'card' as const
static override props = {
w: T.number,
h: T.number,
// You can re-use tldraw built-in styles...
color: DefaultColorStyle,
// ...or your own custom styles.
filter: MyFilterStyle,
}
override isAspectRatioLocked = (_shape: CardShape) => false override isAspectRatioLocked = (_shape: CardShape) => false
override canResize = (_shape: CardShape) => true override canResize = (_shape: CardShape) => true
override canBind = (_shape: CardShape) => true override canBind = (_shape: CardShape) => true
@ -87,17 +95,12 @@ export class CardShapeTool extends BaseBoxShapeTool {
static override id = 'card' static override id = 'card'
static override initial = 'idle' static override initial = 'idle'
override shapeType = 'card' override shapeType = 'card'
} props = {
export const CardShape = defineShape('card', {
util: CardShapeUtil,
// to use a style prop, you need to describe all the props in your shape.
props: {
w: T.number, w: T.number,
h: T.number, h: T.number,
// You can re-use tldraw built-in styles... // You can re-use tldraw built-in styles...
color: DefaultColorStyle, color: DefaultColorStyle,
// ...or your own custom styles. // ...or your own custom styles.
filter: MyFilterStyle, filter: MyFilterStyle,
}, }
}) }

View file

@ -1,10 +1,11 @@
import { Tldraw } from '@tldraw/tldraw' import { Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css' import '@tldraw/tldraw/tldraw.css'
import { CardShape, CardShapeTool } from './CardShape' import { CardShapeTool, CardShapeUtil } from './CardShape'
import { FilterStyleUi } from './FilterStyleUi' import { FilterStyleUi } from './FilterStyleUi'
import { uiOverrides } from './ui-overrides' import { uiOverrides } from './ui-overrides'
const shapes = [CardShape] const customShapeUtils = [CardShapeUtil]
const customTools = [CardShapeTool]
export default function CustomStylesExample() { export default function CustomStylesExample() {
return ( return (
@ -12,8 +13,8 @@ export default function CustomStylesExample() {
<Tldraw <Tldraw
autoFocus autoFocus
persistenceKey="custom-styles-example" persistenceKey="custom-styles-example"
shapes={shapes} shapeUtils={customShapeUtils}
tools={[CardShapeTool]} tools={customTools}
overrides={uiOverrides} overrides={uiOverrides}
> >
<FilterStyleUi /> <FilterStyleUi />

View file

@ -1,5 +1,4 @@
import { track } from '@tldraw/state' import { track, useEditor } from '@tldraw/tldraw'
import { useEditor } from '@tldraw/tldraw'
import { MyFilterStyle } from './CardShape' import { MyFilterStyle } from './CardShape'
export const FilterStyleUi = track(function FilterStyleUi() { export const FilterStyleUi = track(function FilterStyleUi() {

View file

@ -9,7 +9,7 @@ export const uiOverrides: TLUiOverrides = {
kbd: 'c', kbd: 'c',
readonlyOk: false, readonlyOk: false,
onSelect: () => { onSelect: () => {
editor.setSelectedTool('card') editor.setCurrentTool('card')
}, },
} }
return tools return tools

View file

@ -1,15 +0,0 @@
import { defineShape } from '@tldraw/tldraw'
import { CardShapeUtil } from './CardShapeUtil'
import { cardShapeMigrations } from './card-shape-migrations'
import { cardShapeProps } from './card-shape-props'
// A custom shape is a bundle of a shape util, a tool, and props
export const CardShape = defineShape('card', {
// A utility class
util: CardShapeUtil,
// A tool that is used to create and edit the shape (optional)
// A validation schema for the shape's props (optional)
props: cardShapeProps,
// Migrations for upgrading shapes (optional)
migrations: cardShapeMigrations,
})

View file

@ -7,6 +7,8 @@ import {
resizeBox, resizeBox,
} from '@tldraw/tldraw' } from '@tldraw/tldraw'
import { useState } from 'react' import { useState } from 'react'
import { cardShapeMigrations } from './card-shape-migrations'
import { cardShapeProps } from './card-shape-props'
import { ICardShape } from './card-shape-types' import { ICardShape } from './card-shape-types'
// A utility class for the card shape. This is where you define // A utility class for the card shape. This is where you define
@ -15,6 +17,10 @@ import { ICardShape } from './card-shape-types'
export class CardShapeUtil extends ShapeUtil<ICardShape> { export class CardShapeUtil extends ShapeUtil<ICardShape> {
static override type = 'card' as const static override type = 'card' as const
// A validation schema for the shape's props (optional)
static override props = cardShapeProps
// Migrations for upgrading shapes (optional)
static override migrations = cardShapeMigrations
// Flags // Flags
override isAspectRatioLocked = (_shape: ICardShape) => false override isAspectRatioLocked = (_shape: ICardShape) => false

View file

@ -1,5 +1,4 @@
import { DefaultColorStyle, ShapeProps, StyleProp } from '@tldraw/tldraw' import { DefaultColorStyle, ShapeProps, StyleProp, T } from '@tldraw/tldraw'
import { T } from '@tldraw/validate'
import { ICardShape } from './card-shape-types' import { ICardShape } from './card-shape-types'
export const WeightStyle = StyleProp.defineEnum('myApp:weight', { export const WeightStyle = StyleProp.defineEnum('myApp:weight', {

View file

@ -1,18 +1,21 @@
import { Tldraw } from '@tldraw/tldraw' import { Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css' import '@tldraw/tldraw/tldraw.css'
import { CardShapeTool } from './CardShape/CardShapeTool' import { CardShapeTool } from './CardShape/CardShapeTool'
import { customShapes } from './custom-shapes' import { CardShapeUtil } from './CardShape/CardShapeUtil'
import { uiOverrides } from './ui-overrides' import { uiOverrides } from './ui-overrides'
const customShapeUtils = [CardShapeUtil]
const customTools = [CardShapeTool]
export default function CustomConfigExample() { export default function CustomConfigExample() {
return ( return (
<div className="tldraw__editor"> <div className="tldraw__editor">
<Tldraw <Tldraw
autoFocus autoFocus
// Pass in the array of custom shape definitions // Pass in the array of custom shape classes
shapes={customShapes} shapeUtils={customShapeUtils}
// Pass in the array of custom tools // Pass in the array of custom tool classes
tools={[CardShapeTool]} tools={customTools}
// Pass in any overrides to the user interface // Pass in any overrides to the user interface
overrides={uiOverrides} overrides={uiOverrides}
/> />

View file

@ -1,3 +0,0 @@
import { CardShape } from './CardShape/CardShape'
export const customShapes = [CardShape]

View file

@ -12,7 +12,7 @@ export const uiOverrides: TLUiOverrides = {
kbd: 'c', kbd: 'c',
readonlyOk: false, readonlyOk: false,
onSelect: () => { onSelect: () => {
editor.setSelectedTool('card') editor.setCurrentTool('card')
}, },
} }
return tools return tools

View file

@ -1,5 +1,4 @@
import { track } from '@tldraw/state' import { Canvas, Tldraw, track, useEditor } from '@tldraw/tldraw'
import { Canvas, TldrawEditor, defaultShapes, defaultTools, useEditor } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css' import '@tldraw/tldraw/tldraw.css'
import { useEffect } from 'react' import { useEffect } from 'react'
import './custom-ui.css' import './custom-ui.css'
@ -7,10 +6,10 @@ import './custom-ui.css'
export default function CustomUiExample() { export default function CustomUiExample() {
return ( return (
<div className="tldraw__editor"> <div className="tldraw__editor">
<TldrawEditor shapes={defaultShapes} tools={defaultTools} autoFocus> <Tldraw hideUi autoFocus>
<Canvas /> <Canvas />
<CustomUi /> <CustomUi />
</TldrawEditor> </Tldraw>
</div> </div>
) )
} }
@ -24,6 +23,22 @@ const CustomUi = track(() => {
case 'Delete': case 'Delete':
case 'Backspace': { case 'Backspace': {
editor.deleteShapes() editor.deleteShapes()
break
}
case 'v': {
editor.setCurrentTool('select')
break
}
case 'e': {
editor.setCurrentTool('eraser')
break
}
case 'x':
case 'p':
case 'b':
case 'd': {
editor.setCurrentTool('draw')
break
} }
} }
} }
@ -40,21 +55,21 @@ const CustomUi = track(() => {
<button <button
className="custom-button" className="custom-button"
data-isactive={editor.currentToolId === 'select'} data-isactive={editor.currentToolId === 'select'}
onClick={() => editor.setSelectedTool('select')} onClick={() => editor.setCurrentTool('select')}
> >
Select Select
</button> </button>
<button <button
className="custom-button" className="custom-button"
data-isactive={editor.currentToolId === 'draw'} data-isactive={editor.currentToolId === 'draw'}
onClick={() => editor.setSelectedTool('draw')} onClick={() => editor.setCurrentTool('draw')}
> >
Pencil Pencil
</button> </button>
<button <button
className="custom-button" className="custom-button"
data-isactive={editor.currentToolId === 'eraser'} data-isactive={editor.currentToolId === 'eraser'}
onClick={() => editor.setSelectedTool('eraser')} onClick={() => editor.setCurrentTool('eraser')}
> >
Eraser Eraser
</button> </button>

View file

@ -3,7 +3,7 @@ import {
ContextMenu, ContextMenu,
TldrawEditor, TldrawEditor,
TldrawUi, TldrawUi,
defaultShapes, defaultShapeUtils,
defaultTools, defaultTools,
} from '@tldraw/tldraw' } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css' import '@tldraw/tldraw/tldraw.css'
@ -12,7 +12,8 @@ export default function ExplodedExample() {
return ( return (
<div className="tldraw__editor"> <div className="tldraw__editor">
<TldrawEditor <TldrawEditor
shapes={defaultShapes} initialState="select"
shapeUtils={defaultShapeUtils}
tools={defaultTools} tools={defaultTools}
autoFocus autoFocus
persistenceKey="exploded-example" persistenceKey="exploded-example"

View file

@ -1,14 +1,14 @@
import { createShapeId, Tldraw, TLShapePartial } from '@tldraw/tldraw' import { createShapeId, Tldraw, TLShapePartial } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css' import '@tldraw/tldraw/tldraw.css'
import { ErrorShape } from './ErrorShape' import { ErrorShape, ErrorShapeUtil } from './ErrorShape'
const shapes = [ErrorShape] const shapes = [ErrorShapeUtil]
export default function ErrorBoundaryExample() { export default function ErrorBoundaryExample() {
return ( return (
<div className="tldraw__editor"> <div className="tldraw__editor">
<Tldraw <Tldraw
shapes={shapes} shapeUtils={shapes}
tools={[]} tools={[]}
components={{ components={{
ShapeErrorFallback: ({ error }) => <div>Shape error! {String(error)}</div>, // use a custom error fallback for shapes ShapeErrorFallback: ({ error }) => <div>Shape error! {String(error)}</div>, // use a custom error fallback for shapes

View file

@ -1,10 +1,9 @@
import { BaseBoxShapeUtil, TLBaseShape, defineShape } from '@tldraw/tldraw' import { BaseBoxShapeUtil, TLBaseShape } from '@tldraw/tldraw'
export type ErrorShape = TLBaseShape<'error', { w: number; h: number; message: string }> export type ErrorShape = TLBaseShape<'error', { w: number; h: number; message: string }>
export class ErrorShapeUtil extends BaseBoxShapeUtil<ErrorShape> { export class ErrorShapeUtil extends BaseBoxShapeUtil<ErrorShape> {
static override type = 'error' as const static override type = 'error' as const
override type = 'error' as const
getDefaultProps() { getDefaultProps() {
return { message: 'Error!', w: 100, h: 100 } return { message: 'Error!', w: 100, h: 100 }
@ -16,5 +15,3 @@ export class ErrorShapeUtil extends BaseBoxShapeUtil<ErrorShape> {
throw new Error(`Error shape indicator!`) throw new Error(`Error shape indicator!`)
} }
} }
export const ErrorShape = defineShape('error', { util: ErrorShapeUtil })

View file

@ -1,10 +1,6 @@
import { getAssetUrlsByMetaUrl } from '@tldraw/assets/urls' import { getAssetUrlsByMetaUrl } from '@tldraw/assets/urls'
import { import { DefaultErrorFallback, ErrorBoundary, setDefaultUiAssetUrls } from '@tldraw/tldraw'
DefaultErrorFallback, import { setDefaultEditorAssetUrls } from '@tldraw/tldraw/src/lib/utils/assetUrls'
ErrorBoundary,
setDefaultEditorAssetUrls,
setDefaultUiAssetUrls,
} from '@tldraw/tldraw'
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { RouterProvider, createBrowserRouter } from 'react-router-dom' import { RouterProvider, createBrowserRouter } from 'react-router-dom'
@ -28,6 +24,7 @@ import HideUiExample from './9-hide-ui/HideUiExample'
import ExamplesTldrawLogo from './ExamplesTldrawLogo' import ExamplesTldrawLogo from './ExamplesTldrawLogo'
import { ListLink } from './components/ListLink' import { ListLink } from './components/ListLink'
import EndToEnd from './end-to-end/end-to-end' import EndToEnd from './end-to-end/end-to-end'
import OnlyEditorExample from './only-editor/OnlyEditor'
import YjsExample from './yjs/YjsExample' import YjsExample from './yjs/YjsExample'
// This example is only used for end to end tests // This example is only used for end to end tests
@ -50,6 +47,11 @@ export const allExamples: Example[] = [
path: '/develop', path: '/develop',
element: <ExampleBasic />, element: <ExampleBasic />,
}, },
{
title: 'Collaboration (with Yjs)',
path: '/yjs',
element: <YjsExample />,
},
{ {
title: 'Editor API', title: 'Editor API',
path: '/api', path: '/api',
@ -120,11 +122,6 @@ export const allExamples: Example[] = [
path: '/persistence', path: '/persistence',
element: <PersistenceExample />, element: <PersistenceExample />,
}, },
{
title: 'Custom styles',
path: '/yjs',
element: <YjsExample />,
},
{ {
title: 'Custom styles', title: 'Custom styles',
path: '/custom-styles', path: '/custom-styles',
@ -135,6 +132,11 @@ export const allExamples: Example[] = [
path: '/shape-meta', path: '/shape-meta',
element: <ShapeMetaExample />, element: <ShapeMetaExample />,
}, },
{
title: 'Only editor',
path: '/only-editor',
element: <OnlyEditorExample />,
},
// not listed // not listed
{ {
path: '/end-to-end', path: '/end-to-end',

View file

@ -0,0 +1,60 @@
import { StateNode, TLEventHandlers, createShapeId } from '@tldraw/tldraw'
/*
This is a very small example of a state node that implements a "select" tool.
The state handles two events: onPointerDown and onDoubleClick.
When the user points down on the canvas, it deselects all shapes; and when
they point a shape it selects that shape. When the user double clicks on the
canvas, it creates a new shape; and when they double click on a shape, it
deletes that shape.
*/
export class MicroSelectTool extends StateNode {
static override id = 'select'
override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => {
const { editor } = this
switch (info.target) {
case 'canvas': {
editor.selectNone()
break
}
case 'shape': {
editor.select(info.shape.id)
break
}
}
}
override onDoubleClick: TLEventHandlers['onDoubleClick'] = (info) => {
const { editor } = this
if (info.phase !== 'up') return
switch (info.target) {
case 'canvas': {
const { currentPagePoint } = editor.inputs
editor.createShapes([
{
id: createShapeId(),
type: 'box',
x: currentPagePoint.x - 50,
y: currentPagePoint.y - 50,
props: {
w: 100,
h: 100,
},
},
])
break
}
case 'shape': {
editor.deleteShapes([info.shape.id])
break
}
}
}
}

View file

@ -0,0 +1,31 @@
import { BaseBoxShapeUtil, HTMLContainer, TLBaseShape } from '@tldraw/tldraw'
export type MiniBoxShape = TLBaseShape<'box', { w: number; h: number; color: string }>
export class MiniBoxShapeUtil extends BaseBoxShapeUtil<MiniBoxShape> {
static override type = 'box'
override getDefaultProps(): MiniBoxShape['props'] {
return { w: 100, h: 100, color: '#efefef' }
}
component(shape: MiniBoxShape) {
return (
<HTMLContainer>
<div
style={{
width: shape.props.w,
height: shape.props.h,
border: '1px solid black',
backgroundColor: shape.props.color,
pointerEvents: 'all',
}}
/>
</HTMLContainer>
)
}
indicator(shape: MiniBoxShape) {
return <rect width={shape.props.w} height={shape.props.h} />
}
}

View file

@ -0,0 +1,122 @@
import { StateNode, TLEventHandlers, TLUnknownShape, createShapeId } from '@tldraw/tldraw'
/*
This is a bigger example of a state node that implements a "select" tool.
The state has three children: idle, pointing, and dragging. Only one child
state can be "active" at a time. The parent state's initial active state is
"idle". Certain events received by the child states will cause the parent
state to transition to another child state, making that state active instead.
Note that when `transition()` is called, the parent state will call the new
active state(s)'s `onEnter` method with the second argument passed to the
transition method. This is useful for passing data between states.
*/
class IdleState extends StateNode {
static override id = 'idle'
override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => {
const { editor } = this
switch (info.target) {
case 'canvas': {
editor.selectNone()
break
}
case 'selection': {
this.parent.transition('pointing', info)
break
}
case 'shape': {
if (editor.inputs.shiftKey) {
editor.select(...editor.selectedIds, info.shape.id)
} else {
if (!editor.isSelected(info.shape.id)) {
editor.select(info.shape.id)
}
this.parent.transition('pointing', info)
}
break
}
}
}
override onDoubleClick: TLEventHandlers['onDoubleClick'] = (info) => {
const { editor } = this
if (info.phase !== 'up') return
switch (info.target) {
case 'canvas': {
const { currentPagePoint } = editor.inputs
editor.createShapes([
{
id: createShapeId(),
type: 'box',
x: currentPagePoint.x - 50,
y: currentPagePoint.y - 50,
props: {
w: 100,
h: 100,
},
},
])
break
}
case 'shape': {
editor.deleteShapes([info.shape.id])
break
}
}
}
}
class PointingState extends StateNode {
static override id = 'pointing'
override onPointerUp: TLEventHandlers['onPointerUp'] = (info) => {
this.parent.transition('idle', info)
}
override onPointerMove: TLEventHandlers['onPointerUp'] = () => {
if (this.editor.inputs.isDragging) {
this.parent.transition('dragging', { shapes: [...this.editor.selectedShapes] })
}
}
}
class DraggingState extends StateNode {
static override id = 'dragging'
private initialDraggingShapes = [] as TLUnknownShape[]
override onEnter = (info: { shapes: TLUnknownShape[] }) => {
this.initialDraggingShapes = info.shapes
}
override onPointerUp: TLEventHandlers['onPointerUp'] = (info) => {
this.parent.transition('idle', info)
}
override onPointerMove: TLEventHandlers['onPointerUp'] = () => {
const { initialDraggingShapes } = this
const { originPagePoint, currentPagePoint } = this.editor.inputs
this.editor.updateShapes(
initialDraggingShapes.map((shape) => {
return {
...shape,
x: shape.x + (currentPagePoint.x - originPagePoint.x),
y: shape.y + (currentPagePoint.y - originPagePoint.y),
}
})
)
}
}
export class MiniSelectTool extends StateNode {
static override id = 'select'
static override children = () => [IdleState, PointingState, DraggingState]
static override initial = 'idle'
}

View file

@ -0,0 +1,51 @@
/* eslint-disable import/no-extraneous-dependencies */
import { Editor, PositionedOnCanvas, TldrawEditor, createShapeId, track } from '@tldraw/editor'
import '@tldraw/editor/editor.css'
import { MiniBoxShapeUtil } from './MiniBoxShape'
import { MiniSelectTool } from './MiniSelectTool'
const myTools = [MiniSelectTool]
const myShapeUtils = [MiniBoxShapeUtil]
export default function OnlyEditorExample() {
return (
<div className="tldraw__editor">
<TldrawEditor
autoFocus
tools={myTools}
shapeUtils={myShapeUtils}
initialState="select"
onMount={(editor: Editor) => {
editor
.selectAll()
.deleteShapes()
.createShapes([
{
id: createShapeId(),
type: 'box',
x: 100,
y: 100,
},
])
}}
components={{
Background: BackgroundComponent,
}}
/>
</div>
)
}
/**
* This one will move with the camera, just like shapes do.
*/
const BackgroundComponent = track(() => {
return (
<PositionedOnCanvas x={16} y={16}>
<p>Double click to create shapes.</p>
<p>Click or Shift+Click to select shapes.</p>
<p>Click and drag to move shapes.</p>
</PositionedOnCanvas>
)
})

View file

@ -1,5 +1,4 @@
import { track } from '@tldraw/state' import { Tldraw, track, useEditor } from '@tldraw/tldraw'
import { Tldraw, useEditor } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css' import '@tldraw/tldraw/tldraw.css'
import { useYjsStore } from './useYjsStore' import { useYjsStore } from './useYjsStore'

View file

@ -1,18 +1,19 @@
import { computed, react, transact } from '@tldraw/state'
import { import {
DocumentRecordType, DocumentRecordType,
InstancePresenceRecordType, InstancePresenceRecordType,
PageRecordType, PageRecordType,
TLAnyShapeUtilConstructor,
TLDocument, TLDocument,
TLInstancePresence, TLInstancePresence,
TLPageId, TLPageId,
TLRecord, TLRecord,
TLShapeInfo,
TLStoreWithStatus, TLStoreWithStatus,
computed,
createPresenceStateDerivation, createPresenceStateDerivation,
createTLStore, createTLStore,
defaultShapes, defaultShapeUtils,
getUserPreferences, getUserPreferences,
react,
} from '@tldraw/tldraw' } from '@tldraw/tldraw'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { WebsocketProvider } from 'y-websocket' import { WebsocketProvider } from 'y-websocket'
@ -21,14 +22,16 @@ import * as Y from 'yjs'
export function useYjsStore({ export function useYjsStore({
roomId = 'example', roomId = 'example',
hostUrl = process.env.NODE_ENV === 'development' ? 'ws://localhost:1234' : 'wss://demos.yjs.dev', hostUrl = process.env.NODE_ENV === 'development' ? 'ws://localhost:1234' : 'wss://demos.yjs.dev',
shapes = [], shapeUtils = [],
}: Partial<{ }: Partial<{
hostUrl: string hostUrl: string
roomId: string roomId: string
version: number version: number
shapes?: TLShapeInfo[] shapeUtils: TLAnyShapeUtilConstructor[]
}>) { }>) {
const [store] = useState(() => createTLStore({ shapes: [...defaultShapes, ...shapes] })) const [store] = useState(() =>
createTLStore({ shapeUtils: [...defaultShapeUtils, ...shapeUtils] })
)
const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({ status: 'loading' }) const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({ status: 'loading' })
const { doc, room, yRecords } = useMemo(() => { const { doc, room, yRecords } = useMemo(() => {
@ -75,7 +78,7 @@ export function useYjsStore({
// is empty, initialize the yjs doc with the default store records. // is empty, initialize the yjs doc with the default store records.
if (yRecords.size === 0) { if (yRecords.size === 0) {
// Create the initial store records // Create the initial store records
transact(() => { Y.transact(doc, () => {
store.clear() store.clear()
store.put([ store.put([
DocumentRecordType.create({ DocumentRecordType.create({
@ -97,7 +100,7 @@ export function useYjsStore({
}) })
} else { } else {
// Replace the store records with the yjs doc records // Replace the store records with the yjs doc records
transact(() => { Y.transact(doc, () => {
store.clear() store.clear()
store.put([...yRecords.values()]) store.put([...yRecords.values()])
}) })

View file

@ -34,11 +34,7 @@
}, },
"devDependencies": { "devDependencies": {
"@tldraw/assets": "workspace:*", "@tldraw/assets": "workspace:*",
"@tldraw/editor": "workspace:*",
"@tldraw/file-format": "workspace:*",
"@tldraw/tldraw": "workspace:*", "@tldraw/tldraw": "workspace:*",
"@tldraw/ui": "workspace:*",
"@tldraw/utils": "workspace:*",
"@types/fs-extra": "^11.0.1", "@types/fs-extra": "^11.0.1",
"@types/node": "^17.0.14", "@types/node": "^17.0.14",
"@types/react": "^18.0.24", "@types/react": "^18.0.24",
@ -50,7 +46,6 @@
"esbuild": "^0.18.3", "esbuild": "^0.18.3",
"fs-extra": "^11.1.0", "fs-extra": "^11.1.0",
"lazyrepo": "0.0.0-alpha.27", "lazyrepo": "0.0.0-alpha.27",
"nanoid": "4.0.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"tslib": "^2.4.0" "tslib": "^2.4.0"

View file

@ -1,9 +1,9 @@
import esbuild from 'esbuild' import esbuild from 'esbuild'
import fs from 'fs' import fs from 'fs'
import fse from 'fs-extra' import fse, { exists } from 'fs-extra'
import path from 'path' import path from 'path'
import { logEnv } from '../../vscode-script-utils/cli' import { logEnv } from './cli'
import { exists, getDirname } from '../../vscode-script-utils/path' import { getDirname } from './path'
const rootDir = getDirname(import.meta.url, '../') const rootDir = getDirname(import.meta.url, '../')
const log = logEnv('editor') const log = logEnv('editor')

View file

@ -1,11 +1,11 @@
import dotenv from 'dotenv' import dotenv from 'dotenv'
import esbuild from 'esbuild' import esbuild from 'esbuild'
import fs from 'fs' import fs from 'fs'
import fse from 'fs-extra' import fse, { exists } from 'fs-extra'
import path from 'path' import path from 'path'
import { logEnv } from '../../vscode-script-utils/cli' import { logEnv } from './cli'
import { copyEditor } from '../../vscode-script-utils/helpers' import { copyEditor } from './helpers'
import { exists, getDirname } from '../../vscode-script-utils/path' import { getDirname } from './path'
dotenv.config() dotenv.config()
const rootDir = getDirname(import.meta.url, '../') const rootDir = getDirname(import.meta.url, '../')

View file

@ -3,7 +3,7 @@ import fse from 'fs-extra'
import { join } from 'path' import { join } from 'path'
import { exists, getDirname } from './path' import { exists, getDirname } from './path'
const vscodeDir = getDirname(import.meta.url, '../') const vscodeDir = getDirname(import.meta.url, '../../')
export async function copyEditor({ log }: { log: (opts: any) => void }) { export async function copyEditor({ log }: { log: (opts: any) => void }) {
const editorRoot = join(vscodeDir, 'editor') const editorRoot = join(vscodeDir, 'editor')

View file

@ -1,13 +1,15 @@
import { useEditor } from '@tldraw/editor' import {
import { parseAndLoadDocument, serializeTldrawJson } from '@tldraw/file-format' debounce,
import { useDefaultHelpers } from '@tldraw/ui' parseAndLoadDocument,
import { debounce } from '@tldraw/utils' serializeTldrawJson,
useDefaultHelpers,
useEditor,
} from '@tldraw/tldraw'
import React from 'react' import React from 'react'
import '../public/index.css'
import { vscode } from './utils/vscode'
// @ts-ignore // @ts-ignore
import type { VscodeMessage } from '../../messages' import type { VscodeMessage } from '../../messages'
import '../public/index.css'
import { vscode } from './utils/vscode'
export const ChangeResponder = () => { export const ChangeResponder = () => {
const editor = useEditor() const editor = useEditor()

View file

@ -1,6 +1,4 @@
import { useEditor } from '@tldraw/editor' import { parseAndLoadDocument, useDefaultHelpers, useEditor } from '@tldraw/tldraw'
import { parseAndLoadDocument } from '@tldraw/file-format'
import { useDefaultHelpers } from '@tldraw/ui'
import React from 'react' import React from 'react'
import { vscode } from './utils/vscode' import { vscode } from './utils/vscode'

View file

@ -1,20 +1,17 @@
import {
Canvas,
Editor,
ErrorBoundary,
TldrawEditor,
defaultShapes,
defaultTools,
setRuntimeOverrides,
} from '@tldraw/editor'
import { linksUiOverrides } from './utils/links' import { linksUiOverrides } from './utils/links'
// eslint-disable-next-line import/no-internal-modules // eslint-disable-next-line import/no-internal-modules
import '@tldraw/editor/editor.css' import '@tldraw/tldraw/tldraw.css'
import { ContextMenu, TLUiMenuSchema, TldrawUi } from '@tldraw/ui'
// eslint-disable-next-line import/no-internal-modules
import '@tldraw/ui/ui.css'
// eslint-disable-next-line import/no-internal-modules // eslint-disable-next-line import/no-internal-modules
import { getAssetUrlsByImport } from '@tldraw/assets/imports' import { getAssetUrlsByImport } from '@tldraw/assets/imports'
import {
Editor,
ErrorBoundary,
TLUiMenuSchema,
Tldraw,
defaultShapeTools,
defaultShapeUtils,
setRuntimeOverrides,
} from '@tldraw/tldraw'
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { VscodeMessage } from '../../messages' import { VscodeMessage } from '../../messages'
import '../public/index.css' import '../public/index.css'
@ -128,26 +125,22 @@ function TldrawInner({ uri, assetSrc, isDarkMode, fileContents }: TLDrawInnerPro
const assetUrls = useMemo(() => getAssetUrlsByImport({ baseUrl: assetSrc }), [assetSrc]) const assetUrls = useMemo(() => getAssetUrlsByImport({ baseUrl: assetSrc }), [assetSrc])
const handleMount = useCallback((editor: Editor) => { const handleMount = useCallback((editor: Editor) => {
editor.externalContentManager.createAssetFromUrl = onCreateAssetFromUrl editor.registerExternalAssetHandler('url', onCreateAssetFromUrl)
}, []) }, [])
return ( return (
<TldrawEditor <Tldraw
shapes={defaultShapes} shapeUtils={defaultShapeUtils}
tools={defaultTools} tools={defaultShapeTools}
assetUrls={assetUrls} assetUrls={assetUrls}
persistenceKey={uri} persistenceKey={uri}
onMount={handleMount} onMount={handleMount}
overrides={[menuOverrides, linksUiOverrides]}
autoFocus autoFocus
> >
{/* <DarkModeHandler themeKind={themeKind} /> */} {/* <DarkModeHandler themeKind={themeKind} /> */}
<TldrawUi assetUrls={assetUrls} overrides={[menuOverrides, linksUiOverrides]}> <FileOpen fileContents={fileContents} forceDarkMode={isDarkMode} />
<FileOpen fileContents={fileContents} forceDarkMode={isDarkMode} /> <ChangeResponder />
<ChangeResponder /> </Tldraw>
<ContextMenu>
<Canvas />
</ContextMenu>
</TldrawUi>
</TldrawEditor>
) )
} }

View file

@ -1,8 +1,13 @@
import { AssetRecordType, Editor, TLAsset, truncateStringWithEllipsis } from '@tldraw/editor' import { AssetRecordType, TLAsset, TLExternalAssetContent, getHashForString } from '@tldraw/tldraw'
import { getHashForString } from '@tldraw/utils'
import { rpc } from './rpc' import { rpc } from './rpc'
export async function onCreateAssetFromUrl(editor: Editor, url: string): Promise<TLAsset> { export const truncateStringWithEllipsis = (str: string, maxLength: number) => {
return str.length <= maxLength ? str : str.substring(0, maxLength - 3) + '...'
}
export async function onCreateAssetFromUrl({
url,
}: TLExternalAssetContent & { type: 'url' }): Promise<TLAsset> {
try { try {
// First, try to get the data from vscode // First, try to get the data from vscode
const meta = await rpc('vscode:bookmark', { url }) const meta = await rpc('vscode:bookmark', { url })

View file

@ -1,4 +1,4 @@
import { menuGroup, menuItem, TLUiOverrides } from '@tldraw/ui' import { menuGroup, menuItem, TLUiOverrides } from '@tldraw/tldraw'
import { openUrl } from './openUrl' import { openUrl } from './openUrl'
export const GITHUB_URL = 'https://github.com/tldraw/tldraw' export const GITHUB_URL = 'https://github.com/tldraw/tldraw'

View file

@ -1,4 +1,4 @@
import { nanoid } from 'nanoid' import { uniqueId } from '@tldraw/tldraw'
import type { VscodeMessagePairs } from '../../../messages' import type { VscodeMessagePairs } from '../../../messages'
import { vscode } from './vscode' import { vscode } from './vscode'
@ -26,7 +26,7 @@ export function rpc(
type ErrorType = VscodeMessagePairs[typeof id]['error'] type ErrorType = VscodeMessagePairs[typeof id]['error']
const type = (id + '/request') as RequestType['type'] const type = (id + '/request') as RequestType['type']
const uuid = nanoid() const uuid = uniqueId()
return new Promise<ResponseType['data']>((resolve, reject) => { return new Promise<ResponseType['data']>((resolve, reject) => {
const inMessage = { const inMessage = {
uuid, uuid,

View file

@ -22,11 +22,6 @@
"skipDefaultLibCheck": true, "skipDefaultLibCheck": true,
"experimentalDecorators": true "experimentalDecorators": true
}, },
"include": ["src", "../messages", "scripts", "../vscode-script-utils"], "include": ["src", "../messages", "scripts"],
"references": [ "references": [{ "path": "../../../packages/tldraw" }]
{ "path": "../../../packages/file-format" },
{ "path": "../../../packages/ui" },
{ "path": "../../../packages/editor" },
{ "path": "../../../packages/utils" }
]
} }

View file

@ -131,8 +131,7 @@
}, },
"devDependencies": { "devDependencies": {
"@tldraw/editor": "workspace:*", "@tldraw/editor": "workspace:*",
"@tldraw/file-format": "workspace:*", "@tldraw/tldraw": "workspace:*",
"@tldraw/store": "workspace:*",
"@types/fs-extra": "^11.0.1", "@types/fs-extra": "^11.0.1",
"@types/node-fetch": "^2.6.2", "@types/node-fetch": "^2.6.2",
"@types/vscode": "^1.75.1", "@types/vscode": "^1.75.1",
@ -152,7 +151,6 @@
}, },
"gitHead": "4b1137849ad07da36fc8f0f19cb64e7535a79296", "gitHead": "4b1137849ad07da36fc8f0f19cb64e7535a79296",
"dependencies": { "dependencies": {
"nanoid": "4.0.2",
"node-fetch": "^2.0.0" "node-fetch": "^2.0.0"
} }
} }

View file

@ -1,6 +1,6 @@
import esbuild from 'esbuild' import esbuild from 'esbuild'
import { logEnv } from '../../vscode-script-utils/cli' import { logEnv } from './cli'
import { copyEditor, removeDistDirectory } from '../../vscode-script-utils/helpers' import { copyEditor, removeDistDirectory } from './helpers'
const log = logEnv('extension') const log = logEnv('extension')

View file

@ -0,0 +1,58 @@
import path from 'path'
const displayRelative = (from: string, to: string) => {
const outpath = path.relative(from, to)
if (!outpath.match(/^\./)) {
return `./${outpath}`
}
return outpath
}
type LogDef =
| { cmd: 'remove'; env: string; args: { target: string } }
| { cmd: 'copy'; env: string; args: { source: string; dest: string } }
| { cmd: 'esbuild'; env: string; args: { entryPoints: string[] } }
| { cmd: 'esbuild:success'; env: string; args: any }
| { cmd: 'esbuild:error'; env: string; args: { error: string } }
| { cmd: 'esbuild:serve'; env: string; args: { host: string; port: number | string } }
export function log(def: LogDef) {
const printStderr = (icon: string, cmd: string, ...args: unknown[]) => {
console.error(`${icon} [${def.env ?? 'unknown'}/${cmd}]`, ...args)
}
if (def.cmd === 'remove') {
const { target } = def.args
printStderr('🗑 ', 'remove', displayRelative(process.cwd(), target))
} else if (def.cmd === 'copy') {
const { source, dest } = def.args
printStderr(
'🏠',
'copy',
`${displayRelative(process.cwd(), source)} -> ${displayRelative(process.cwd(), dest)}`
)
} else if (def.cmd === 'esbuild') {
printStderr(
'🤖',
'esbuild',
`${def.args.entryPoints.map((pathname) => displayRelative(process.cwd(), pathname))}`
)
} else if (def.cmd === 'esbuild:success') {
printStderr('✅', `esbuild`, `build successful (${new Date().toISOString()})`)
} else if (def.cmd === 'esbuild:error') {
printStderr(``, `esbuild`, `error`)
console.error(def.args.error)
} else if (def.cmd === 'esbuild:serve') {
const { host = 'localhost', port } = def.args
printStderr(`🌎`, `esbuild`, `serving <http://${host}:${port}>`)
} else {
// @ts-ignore
printStderr(``, def.cmd, JSON.stringify(def.args))
}
}
export function logEnv(env: string) {
return (opts: any) => {
log({ ...opts, env })
}
}

View file

@ -1,8 +1,8 @@
import esbuild from 'esbuild' import esbuild from 'esbuild'
import { join } from 'path' import { join } from 'path'
import { logEnv } from '../../vscode-script-utils/cli' import { logEnv } from './cli'
import { copyEditor, removeDistDirectory } from '../../vscode-script-utils/helpers' import { copyEditor, removeDistDirectory } from './helpers'
import { getDirname } from '../../vscode-script-utils/path' import { getDirname } from './path'
const rootDir = getDirname(import.meta.url, '../') const rootDir = getDirname(import.meta.url, '../')
const log = logEnv('extension') const log = logEnv('extension')

View file

@ -0,0 +1,25 @@
import fs from 'fs'
import fse from 'fs-extra'
import { join } from 'path'
import { exists, getDirname } from './path'
const vscodeDir = getDirname(import.meta.url, '../../')
export async function copyEditor({ log }: { log: (opts: any) => void }) {
const editorRoot = join(vscodeDir, 'editor')
const extensionRoot = join(vscodeDir, 'extension')
const source = join(editorRoot, 'dist')
const dest = join(extensionRoot, 'editor')
log({ cmd: 'copy', args: { source, dest } })
await fse.copy(source, dest)
}
export async function removeDistDirectory({ log }: { log: (opts: any) => void }) {
const target = join(vscodeDir, 'extension', 'dist')
if (await exists(target)) {
log({ cmd: 'remove', args: { target } })
await fs.promises.rm(target, { recursive: true })
}
}

View file

@ -0,0 +1,16 @@
import fs from 'fs'
import path from 'path'
export function getDirname(metaUrl: string, targetPath: string) {
const dirname = path.dirname(metaUrl.replace('file://', ''))
return path.normalize(path.join(dirname, targetPath))
}
export async function exists(targetFolder: string) {
try {
await fs.promises.access(targetFolder)
return true
} catch (err) {
return false
}
}

View file

@ -1,4 +1,4 @@
import { TldrawFile } from '@tldraw/file-format' import { TldrawFile } from '@tldraw/tldraw'
import * as vscode from 'vscode' import * as vscode from 'vscode'
import { defaultFileContents, fileExists, loadFile } from './file' import { defaultFileContents, fileExists, loadFile } from './file'
import { nicelog } from './utils' import { nicelog } from './utils'

View file

@ -1,8 +1,7 @@
import { nanoid } from 'nanoid' import { uniqueId } from '@tldraw/tldraw'
import * as vscode from 'vscode' import * as vscode from 'vscode'
import { TLDrawDocument } from './TldrawDocument' import { TLDrawDocument } from './TldrawDocument'
import { GlobalStateKeys, WebViewMessageHandler } from './WebViewMessageHandler' import { GlobalStateKeys, WebViewMessageHandler } from './WebViewMessageHandler'
// @ts-ignore
export class TldrawWebviewManager { export class TldrawWebviewManager {
private disposables: vscode.Disposable[] = [] private disposables: vscode.Disposable[] = []
@ -15,7 +14,7 @@ export class TldrawWebviewManager {
) { ) {
let userId = context.globalState.get(GlobalStateKeys.UserId) let userId = context.globalState.get(GlobalStateKeys.UserId)
if (!userId) { if (!userId) {
userId = 'user:' + nanoid() userId = 'user:' + uniqueId()
context.globalState.update(GlobalStateKeys.UserId, userId) context.globalState.update(GlobalStateKeys.UserId, userId)
} }

View file

@ -1,10 +1,10 @@
import { UnknownRecord } from '@tldraw/store'
import { isEqual } from 'lodash' import { isEqual } from 'lodash'
import fetch from 'node-fetch' import fetch from 'node-fetch'
import * as vscode from 'vscode' import * as vscode from 'vscode'
import { TLDrawDocument } from './TldrawDocument' import { TLDrawDocument } from './TldrawDocument'
import { loadFile } from './file' import { loadFile } from './file'
import { UnknownRecord } from '@tldraw/tldraw'
// @ts-ignore // @ts-ignore
import type { VscodeMessage } from '../../messages' import type { VscodeMessage } from '../../messages'
import { nicelog } from './utils' import { nicelog } from './utils'

View file

@ -1,17 +1,16 @@
import { createTLStore, defaultShapes } from '@tldraw/editor' import { TldrawFile, createTLStore, defaultShapeUtils } from '@tldraw/tldraw'
import { TldrawFile } from '@tldraw/file-format'
import * as vscode from 'vscode' import * as vscode from 'vscode'
import { nicelog } from './utils' import { nicelog } from './utils'
export const defaultFileContents: TldrawFile = { export const defaultFileContents: TldrawFile = {
tldrawFileFormatVersion: 1, tldrawFileFormatVersion: 1,
schema: createTLStore({ shapes: defaultShapes }).schema.serialize(), schema: createTLStore({ shapeUtils: defaultShapeUtils }).schema.serialize(),
records: [], records: [],
} }
export const fileContentWithErrors: TldrawFile = { export const fileContentWithErrors: TldrawFile = {
tldrawFileFormatVersion: 1, tldrawFileFormatVersion: 1,
schema: createTLStore({ shapes: defaultShapes }).schema.serialize(), schema: createTLStore({ shapeUtils: defaultShapeUtils }).schema.serialize(),
records: [{ typeName: 'shape', id: null } as any], records: [{ typeName: 'shape', id: null } as any],
} }

View file

@ -22,6 +22,6 @@
"skipDefaultLibCheck": true, "skipDefaultLibCheck": true,
"experimentalDecorators": true "experimentalDecorators": true
}, },
"include": ["src", "../messages", "scripts", "../vscode-script-utils"], "include": ["src", "../messages", "scripts"],
"references": [{ "path": "../../../packages/file-format" }] "references": [{ "path": "../../../packages/tldraw" }]
} }

View file

@ -89,7 +89,7 @@ const MyCustomShapes = [MyCardShape]
export default function () { export default function () {
return ( return (
<div style={{ position: 'fixed', inset: 0 }}> <div style={{ position: 'fixed', inset: 0 }}>
<Tldraw shapes={MyCustomShapes}/> <Tldraw shapeUtils={MyCustomShapes}/>
</div> </div>
) )
} }
@ -101,7 +101,7 @@ The [`defineShape`](/gen/editor/defineShape) function can also be used to includ
export default function () { export default function () {
return ( return (
<div style={{ position: 'fixed', inset: 0 }}> <div style={{ position: 'fixed', inset: 0 }}>
<Tldraw shapes={MyCustomShapes} onMount={editor => { <Tldraw shapeUtils={MyCustomShapes} onMount={editor => {
editor.createShapes([{ type: "card" }]) editor.createShapes([{ type: "card" }])
}}/> }}/>
</div> </div>

View file

@ -94,5 +94,8 @@
}, },
"resolutions": { "resolutions": {
"@microsoft/api-extractor@^7.35.4": "patch:@microsoft/api-extractor@npm%3A7.35.4#./.yarn/patches/@microsoft-api-extractor-npm-7.35.4-5f4f0357b4.patch" "@microsoft/api-extractor@^7.35.4": "patch:@microsoft/api-extractor@npm%3A7.35.4#./.yarn/patches/@microsoft-api-extractor-npm-7.35.4-5f4f0357b4.patch"
},
"dependencies": {
"svgo": "^3.0.2"
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -245,14 +245,11 @@ input,
pointer-events: none; pointer-events: none;
} }
.tlui-following { .tl-positioned {
display: block;
position: absolute; position: absolute;
inset: 0px; top: 0px;
border-width: 2px; left: 0px;
border-style: solid; transform-origin: top left;
z-index: 9999999;
pointer-events: none;
} }
/* ------------------- Background ------------------- */ /* ------------------- Background ------------------- */

View file

@ -45,24 +45,21 @@
"lint": "yarn run -T tsx ../../scripts/lint.ts" "lint": "yarn run -T tsx ../../scripts/lint.ts"
}, },
"dependencies": { "dependencies": {
"@tldraw/indices": "workspace:*",
"@tldraw/primitives": "workspace:*",
"@tldraw/state": "workspace:*", "@tldraw/state": "workspace:*",
"@tldraw/store": "workspace:*", "@tldraw/store": "workspace:*",
"@tldraw/tlschema": "workspace:*", "@tldraw/tlschema": "workspace:*",
"@tldraw/utils": "workspace:*", "@tldraw/utils": "workspace:*",
"@tldraw/validate": "workspace:*", "@tldraw/validate": "workspace:*",
"@types/canvas-size": "^1.2.0", "@types/core-js": "^2.5.5",
"@use-gesture/react": "^10.2.27", "@use-gesture/react": "^10.2.27",
"canvas-size": "^1.2.6",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"escape-string-regexp": "^5.0.0", "core-js": "^3.31.1",
"eventemitter3": "^4.0.7", "eventemitter3": "^4.0.7",
"idb": "^7.1.1", "idb": "^7.1.1",
"is-plain-object": "^5.0.0", "is-plain-object": "^5.0.0",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"lodash.uniq": "^4.5.0", "lodash.uniq": "^4.5.0",
"nanoid": "4.0.2" "nanoid": "^3.3.6"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^18", "react": "^18",
@ -95,7 +92,7 @@
"^.+\\.*.css$" "^.+\\.*.css$"
], ],
"transformIgnorePatterns": [ "transformIgnorePatterns": [
"node_modules/(?!(nanoid|escape-string-regexp)/)" "node_modules/(?!(nanoid)/)"
], ],
"moduleNameMapper": { "moduleNameMapper": {
"^~(.*)": "<rootDir>/src/$1", "^~(.*)": "<rootDir>/src/$1",

View file

@ -1,23 +1,30 @@
// Important! don't move this tlschema re-export to lib/index.ts, doing so causes esbuild to produce // Important! don't move this tlschema re-export to lib/index.ts, doing so causes esbuild to produce
// incorrect output. https://github.com/evanw/esbuild/issues/1737 // incorrect output. https://github.com/evanw/esbuild/issues/1737
// eslint-disable-next-line local/no-export-star
export * from '@tldraw/indices'
export { export {
EMPTY_ARRAY,
atom, atom,
computed, computed,
react, react,
track, track,
transact,
transaction,
useComputed, useComputed,
useQuickReactor, useQuickReactor,
useReactor, useReactor,
useValue, useValue,
whyAmIRunning, whyAmIRunning,
type Atom,
type Signal,
} from '@tldraw/state' } from '@tldraw/state'
export { defineMigrations } from '@tldraw/store' // eslint-disable-next-line local/no-export-star
export * from '@tldraw/store'
// eslint-disable-next-line local/no-export-star // eslint-disable-next-line local/no-export-star
export * from '@tldraw/tlschema' export * from '@tldraw/tlschema'
export { getHashForString } from '@tldraw/utils' // eslint-disable-next-line local/no-export-star
export * from '@tldraw/utils'
// eslint-disable-next-line local/no-export-star
export * from '@tldraw/validate'
export { export {
ErrorScreen, ErrorScreen,
LoadingScreen, LoadingScreen,
@ -26,20 +33,16 @@ export {
type TldrawEditorBaseProps, type TldrawEditorBaseProps,
type TldrawEditorProps, type TldrawEditorProps,
} from './lib/TldrawEditor' } from './lib/TldrawEditor'
export {
defaultEditorAssetUrls,
setDefaultEditorAssetUrls,
type TLEditorAssetUrls,
} from './lib/assetUrls'
export { Canvas } from './lib/components/Canvas' export { Canvas } from './lib/components/Canvas'
export { DefaultErrorFallback } from './lib/components/DefaultErrorFallback'
export { export {
ErrorBoundary, ErrorBoundary,
OptionalErrorBoundary, OptionalErrorBoundary,
type TLErrorBoundaryProps, type TLErrorBoundaryProps,
} from './lib/components/ErrorBoundary' } from './lib/components/ErrorBoundary'
export { HTMLContainer, type HTMLContainerProps } from './lib/components/HTMLContainer' export { HTMLContainer, type HTMLContainerProps } from './lib/components/HTMLContainer'
export { PositionedOnCanvas } from './lib/components/PositionedOnCanvas'
export { SVGContainer, type SVGContainerProps } from './lib/components/SVGContainer' export { SVGContainer, type SVGContainerProps } from './lib/components/SVGContainer'
export { DefaultErrorFallback } from './lib/components/default-components/DefaultErrorFallback'
export { export {
TAB_ID, TAB_ID,
createSessionStateSnapshotSignal, createSessionStateSnapshotSignal,
@ -60,39 +63,35 @@ export {
type TLStoreOptions, type TLStoreOptions,
} from './lib/config/createTLStore' } from './lib/config/createTLStore'
export { createTLUser } from './lib/config/createTLUser' export { createTLUser } from './lib/config/createTLUser'
export { coreShapes, defaultShapes } from './lib/config/defaultShapes' export { coreShapes, type TLAnyShapeUtilConstructor } from './lib/config/defaultShapes'
export { defaultTools } from './lib/config/defaultTools'
export { defineShape, type TLShapeInfo } from './lib/config/defineShape'
export { export {
ANIMATION_MEDIUM_MS, ANIMATION_MEDIUM_MS,
ANIMATION_SHORT_MS, ANIMATION_SHORT_MS,
CAMERA_SLIDE_FRICTION,
DEFAULT_ANIMATION_OPTIONS, DEFAULT_ANIMATION_OPTIONS,
DOUBLE_CLICK_DURATION, DOUBLE_CLICK_DURATION,
DRAG_DISTANCE, DRAG_DISTANCE,
GRID_INCREMENT, GRID_INCREMENT,
GRID_STEPS, GRID_STEPS,
HAND_TOOL_FRICTION,
HASH_PATTERN_ZOOM_NAMES, HASH_PATTERN_ZOOM_NAMES,
MAJOR_NUDGE_FACTOR, MAJOR_NUDGE_FACTOR,
MAX_ASSET_HEIGHT,
MAX_ASSET_WIDTH,
MAX_PAGES, MAX_PAGES,
MAX_SHAPES_PER_PAGE, MAX_SHAPES_PER_PAGE,
MAX_ZOOM, MAX_ZOOM,
MINOR_NUDGE_FACTOR, MINOR_NUDGE_FACTOR,
MIN_ZOOM, MIN_ZOOM,
MULTI_CLICK_DURATION, MULTI_CLICK_DURATION,
REMOVE_SYMBOL,
RICH_TYPES,
SVG_PADDING, SVG_PADDING,
ZOOMS, ZOOMS,
} from './lib/constants' } from './lib/constants'
export { Editor, type TLAnimationOptions, type TLEditorOptions } from './lib/editor/Editor' export { Editor, type TLAnimationOptions, type TLEditorOptions } from './lib/editor/Editor'
export { export {
ExternalContentManager as PlopManager, SnapManager,
type TLExternalContent, type GapsSnapLine,
} from './lib/editor/managers/ExternalContentManager' type PointsSnapLine,
export { ScribbleManager } from './lib/editor/managers/ScribbleManager' type SnapLine,
type SnapPoint,
} from './lib/editor/managers/SnapManager'
export { BaseBoxShapeUtil, type TLBaseBoxShape } from './lib/editor/shapes/BaseBoxShapeUtil' export { BaseBoxShapeUtil, type TLBaseBoxShape } from './lib/editor/shapes/BaseBoxShapeUtil'
export { export {
ShapeUtil, ShapeUtil,
@ -117,38 +116,25 @@ export {
type TLOnTranslateStartHandler, type TLOnTranslateStartHandler,
type TLResizeInfo, type TLResizeInfo,
type TLResizeMode, type TLResizeMode,
type TLShapeUtilCanvasSvgDef,
type TLShapeUtilConstructor, type TLShapeUtilConstructor,
type TLShapeUtilFlag, type TLShapeUtilFlag,
} from './lib/editor/shapes/ShapeUtil' } from './lib/editor/shapes/ShapeUtil'
export { ArrowShape } from './lib/editor/shapes/arrow/ArrowShape'
export { ArrowShapeUtil } from './lib/editor/shapes/arrow/ArrowShapeUtil'
export { BookmarkShape } from './lib/editor/shapes/bookmark/BookmarkShape'
export { BookmarkShapeUtil } from './lib/editor/shapes/bookmark/BookmarkShapeUtil'
export { DrawShape } from './lib/editor/shapes/draw/DrawShape'
export { DrawShapeUtil } from './lib/editor/shapes/draw/DrawShapeUtil'
export { EmbedShape } from './lib/editor/shapes/embed/EmbedShape'
export { EmbedShapeUtil } from './lib/editor/shapes/embed/EmbedShapeUtil'
export { FrameShape } from './lib/editor/shapes/frame/FrameShape'
export { FrameShapeUtil } from './lib/editor/shapes/frame/FrameShapeUtil'
export { GeoShape } from './lib/editor/shapes/geo/GeoShape'
export { GeoShapeUtil } from './lib/editor/shapes/geo/GeoShapeUtil'
export { GroupShape } from './lib/editor/shapes/group/GroupShape'
export { GroupShapeUtil } from './lib/editor/shapes/group/GroupShapeUtil' export { GroupShapeUtil } from './lib/editor/shapes/group/GroupShapeUtil'
export { HighlightShape } from './lib/editor/shapes/highlight/HighlightShape' export { getArrowheadPathForType } from './lib/editor/shapes/shared/arrow/arrowheads'
export { HighlightShapeUtil } from './lib/editor/shapes/highlight/HighlightShapeUtil' export {
export { ImageShape } from './lib/editor/shapes/image/ImageShape' getCurvedArrowHandlePath,
export { ImageShapeUtil } from './lib/editor/shapes/image/ImageShapeUtil' getSolidCurvedArrowPath,
export { LineShape } from './lib/editor/shapes/line/LineShape' } from './lib/editor/shapes/shared/arrow/curved-arrow'
export { LineShapeUtil, getSplineForLineShape } from './lib/editor/shapes/line/LineShapeUtil' export { getArrowTerminalsInArrowSpace } from './lib/editor/shapes/shared/arrow/shared'
export { NoteShape } from './lib/editor/shapes/note/NoteShape' export {
export { NoteShapeUtil } from './lib/editor/shapes/note/NoteShapeUtil' getSolidStraightArrowPath,
getStraightArrowHandlePath,
} from './lib/editor/shapes/shared/arrow/straight-arrow'
export { resizeBox, type ResizeBoxOptions } from './lib/editor/shapes/shared/resizeBox' export { resizeBox, type ResizeBoxOptions } from './lib/editor/shapes/shared/resizeBox'
export { TextShape } from './lib/editor/shapes/text/TextShape'
export { INDENT, TextShapeUtil } from './lib/editor/shapes/text/TextShapeUtil'
export { VideoShape } from './lib/editor/shapes/video/VideoShape'
export { VideoShapeUtil } from './lib/editor/shapes/video/VideoShapeUtil'
export { BaseBoxShapeTool } from './lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool' export { BaseBoxShapeTool } from './lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool'
export { StateNode, type TLStateNodeConstructor } from './lib/editor/tools/StateNode' export { StateNode, type TLStateNodeConstructor } from './lib/editor/tools/StateNode'
export { type SvgExportContext, type SvgExportDef } from './lib/editor/types/SvgExportContext'
export { type TLContent } from './lib/editor/types/clipboard-types' export { type TLContent } from './lib/editor/types/clipboard-types'
export { type TLEventMap, type TLEventMapHandler } from './lib/editor/types/emit-types' export { type TLEventMap, type TLEventMapHandler } from './lib/editor/types/emit-types'
export { export {
@ -184,6 +170,10 @@ export {
type UiEvent, type UiEvent,
type UiEventType, type UiEventType,
} from './lib/editor/types/event-types' } from './lib/editor/types/event-types'
export {
type TLExternalAssetContent,
type TLExternalContent,
} from './lib/editor/types/external-content'
export { export {
type TLCommand, type TLCommand,
type TLCommandHandler, type TLCommandHandler,
@ -192,83 +182,126 @@ export {
} from './lib/editor/types/history-types' } from './lib/editor/types/history-types'
export { type RequiredKeys } from './lib/editor/types/misc-types' export { type RequiredKeys } from './lib/editor/types/misc-types'
export { type TLResizeHandle, type TLSelectionHandle } from './lib/editor/types/selection-types' export { type TLResizeHandle, type TLSelectionHandle } from './lib/editor/types/selection-types'
export { normalizeWheel } from './lib/hooks/shared'
export { useContainer } from './lib/hooks/useContainer' export { useContainer } from './lib/hooks/useContainer'
export { getCursor } from './lib/hooks/useCursor'
export { useEditor } from './lib/hooks/useEditor' export { useEditor } from './lib/hooks/useEditor'
export type { TLEditorComponents } from './lib/hooks/useEditorComponents' export type { TLEditorComponents } from './lib/hooks/useEditorComponents'
export { useIsCropping } from './lib/hooks/useIsCropping'
export { useIsEditing } from './lib/hooks/useIsEditing'
export { useLocalStore } from './lib/hooks/useLocalStore' export { useLocalStore } from './lib/hooks/useLocalStore'
export { usePeerIds } from './lib/hooks/usePeerIds' export { usePeerIds } from './lib/hooks/usePeerIds'
export { usePresence } from './lib/hooks/usePresence' export { usePresence } from './lib/hooks/usePresence'
export { useSelectionEvents } from './lib/hooks/useSelectionEvents'
export { useTLStore } from './lib/hooks/useTLStore' export { useTLStore } from './lib/hooks/useTLStore'
export { useTransform } from './lib/hooks/useTransform'
export {
Box2d,
ROTATE_CORNER_TO_SELECTION_CORNER,
rotateSelectionHandle,
type RotateCorner,
type SelectionCorner,
type SelectionEdge,
type SelectionHandle,
} from './lib/primitives/Box2d'
export { Matrix2d, type Matrix2dModel } from './lib/primitives/Matrix2d'
export { Vec2d, type VecLike } from './lib/primitives/Vec2d'
export { EASINGS } from './lib/primitives/easings'
export {
intersectLineSegmentPolygon,
intersectLineSegmentPolyline,
intersectPolygonPolygon,
linesIntersect,
polygonsIntersect,
} from './lib/primitives/intersect'
export {
EPSILON,
PI,
PI2,
SIN,
TAU,
angleDelta,
approximately,
areAnglesCompatible,
average,
canonicalizeRotation,
clamp,
clampRadians,
degreesToRadians,
getArcLength,
getPointOnCircle,
getPolygonVertices,
getStarBounds,
getSweep,
isAngleBetween,
isSafeFloat,
lerpAngles,
longAngleDist,
perimeterOfEllipse,
pointInBounds,
pointInCircle,
pointInEllipse,
pointInPolygon,
pointInPolyline,
pointInRect,
pointNearToLineSegment,
pointNearToPolyline,
precise,
radiansToDegrees,
rangeIntersection,
shortAngleDist,
snapAngle,
toDomPrecision,
toFixed,
toPrecision,
} from './lib/primitives/utils'
export { export {
ReadonlySharedStyleMap, ReadonlySharedStyleMap,
SharedStyleMap, SharedStyleMap,
type SharedStyle, type SharedStyle,
} from './lib/utils/SharedStylesMap' } from './lib/utils/SharedStylesMap'
export { WeakMapCache } from './lib/utils/WeakMapCache' export { WeakMapCache } from './lib/utils/WeakMapCache'
export { export { dataUrlToFile } from './lib/utils/assets'
ACCEPTED_ASSET_TYPE,
ACCEPTED_IMG_TYPE,
ACCEPTED_VID_TYPE,
containBoxSize,
dataUrlToFile,
getFileMetaData,
getImageSizeFromSrc,
getMediaAssetFromFile,
getResizedImageDataUrl,
getValidHttpURLList,
getVideoSizeFromSrc,
isImage,
isSvgText,
isValidHttpURL,
} from './lib/utils/assets'
export {
checkFlag,
fileToBase64,
getIncrementedName,
isSerializable,
isValidUrl,
snapToGrid,
uniqueId,
} from './lib/utils/data'
export { debugFlags, featureFlags, type DebugFlag } from './lib/utils/debug-flags' export { debugFlags, featureFlags, type DebugFlag } from './lib/utils/debug-flags'
export { export {
getRotatedBoxShadow,
loopToHtmlElement, loopToHtmlElement,
preventDefault, preventDefault,
releasePointerCapture, releasePointerCapture,
setPointerCapture, setPointerCapture,
truncateStringWithEllipsis, stopEventPropagation,
usePrefersReducedMotion,
} from './lib/utils/dom' } from './lib/utils/dom'
export { getIncrementedName } from './lib/utils/getIncrementedName'
export { getPointerInfo } from './lib/utils/getPointerInfo'
export { getSvgPathFromPoints } from './lib/utils/getSvgPathFromPoints'
export { hardResetEditor } from './lib/utils/hardResetEditor'
export { normalizeWheel } from './lib/utils/normalizeWheel'
export { png } from './lib/utils/png'
export { refreshPage } from './lib/utils/refreshPage'
export { export {
getEmbedInfo, getIndexAbove,
getEmbedInfoUnsafely, getIndexBelow,
matchEmbedUrl, getIndexBetween,
matchUrl, getIndices,
type TLEmbedResult, getIndicesAbove,
} from './lib/utils/embeds' getIndicesBelow,
getIndicesBetween,
sortByIndex,
} from './lib/utils/reordering/reordering'
export { export {
downloadDataURLAsFile, applyRotationToSnapshotShapes,
getSvgAsDataUrl, getRotationSnapshot,
getSvgAsDataUrlSync, type TLRotationSnapshot,
getSvgAsImage, } from './lib/utils/rotation'
getSvgAsString,
getTextBoundingBox,
type TLCopyType,
type TLExportType,
} from './lib/utils/export'
export { hardResetEditor } from './lib/utils/hard-reset'
export { isAnimated, isGIF } from './lib/utils/is-gif-animated'
export { refreshPage } from './lib/utils/refresh-page'
export { runtime, setRuntimeOverrides } from './lib/utils/runtime' export { runtime, setRuntimeOverrides } from './lib/utils/runtime'
export {
blobAsString,
correctSpacesToNbsp,
dataTransferItemAsString,
defaultEmptyAs,
} from './lib/utils/string'
export { getPointerInfo, getSvgPathFromStroke, getSvgPathFromStrokePoints } from './lib/utils/svg'
export { type TLStoreWithStatus } from './lib/utils/sync/StoreWithStatus' export { type TLStoreWithStatus } from './lib/utils/sync/StoreWithStatus'
export { hardReset } from './lib/utils/sync/hardReset' export { hardReset } from './lib/utils/sync/hardReset'
export { uniq } from './lib/utils/uniq'
export { uniqueId } from './lib/utils/uniqueId'
export { openWindow } from './lib/utils/window-open' export { openWindow } from './lib/utils/window-open'
/** @polyfills */
import 'core-js/stable/array/at'
import 'core-js/stable/array/flat'
import 'core-js/stable/array/flat-map'
import 'core-js/stable/string/at'
import 'core-js/stable/string/replace-all'

View file

@ -1,6 +1,6 @@
import { SerializedStore, Store } from '@tldraw/store' import { SerializedStore, Store } from '@tldraw/store'
import { TLRecord, TLStore } from '@tldraw/tlschema' import { TLRecord, TLStore } from '@tldraw/tlschema'
import { RecursivePartial, Required, annotateError } from '@tldraw/utils' import { Required, annotateError } from '@tldraw/utils'
import React, { import React, {
memo, memo,
useCallback, useCallback,
@ -10,11 +10,11 @@ import React, {
useSyncExternalStore, useSyncExternalStore,
} from 'react' } from 'react'
import { TLEditorAssetUrls, useDefaultEditorAssetsWithOverrides } from './assetUrls' import { Canvas } from './components/Canvas'
import { DefaultErrorFallback } from './components/DefaultErrorFallback'
import { OptionalErrorBoundary } from './components/ErrorBoundary' import { OptionalErrorBoundary } from './components/ErrorBoundary'
import { DefaultErrorFallback } from './components/default-components/DefaultErrorFallback'
import { TLUser, createTLUser } from './config/createTLUser' import { TLUser, createTLUser } from './config/createTLUser'
import { AnyTLShapeInfo } from './config/defineShape' import { TLAnyShapeUtilConstructor } from './config/defaultShapes'
import { Editor } from './editor/Editor' import { Editor } from './editor/Editor'
import { TLStateNodeConstructor } from './editor/tools/StateNode' import { TLStateNodeConstructor } from './editor/tools/StateNode'
import { ContainerProvider, useContainer } from './hooks/useContainer' import { ContainerProvider, useContainer } from './hooks/useContainer'
@ -29,7 +29,6 @@ import {
import { useEvent } from './hooks/useEvent' import { useEvent } from './hooks/useEvent'
import { useForceUpdate } from './hooks/useForceUpdate' import { useForceUpdate } from './hooks/useForceUpdate'
import { useLocalStore } from './hooks/useLocalStore' import { useLocalStore } from './hooks/useLocalStore'
import { usePreloadAssets } from './hooks/usePreloadAssets'
import { useSafariFocusOutFix } from './hooks/useSafariFocusOutFix' import { useSafariFocusOutFix } from './hooks/useSafariFocusOutFix'
import { useZoomCss } from './hooks/useZoomCss' import { useZoomCss } from './hooks/useZoomCss'
import { TLStoreWithStatus } from './utils/sync/StoreWithStatus' import { TLStoreWithStatus } from './utils/sync/StoreWithStatus'
@ -65,20 +64,15 @@ export interface TldrawEditorBaseProps {
children?: any children?: any
/** /**
* An array of shapes definitions to make available to the editor. * An array of shape utils to use in the editor.
*/ */
shapes?: readonly AnyTLShapeInfo[] shapeUtils?: readonly TLAnyShapeUtilConstructor[]
/** /**
* An array of tools to add to the editor's state chart. * An array of tools to add to the editor's state chart.
*/ */
tools?: readonly TLStateNodeConstructor[] tools?: readonly TLStateNodeConstructor[]
/**
* Urls for the editor to find fonts and other assets.
*/
assetUrls?: RecursivePartial<TLEditorAssetUrls>
/** /**
* Whether to automatically focus the editor when it mounts. * Whether to automatically focus the editor when it mounts.
*/ */
@ -93,6 +87,11 @@ export interface TldrawEditorBaseProps {
* Called when the editor has mounted. * Called when the editor has mounted.
*/ */
onMount?: TLOnMountHandler onMount?: TLOnMountHandler
/**
* The editor's initial state (usually the id of the first active tool).
*/
initialState?: string
} }
/** /**
@ -113,7 +112,7 @@ declare global {
} }
} }
const EMPTY_SHAPES_ARRAY = [] as const const EMPTY_SHAPE_UTILS_ARRAY = [] as const
const EMPTY_TOOLS_ARRAY = [] as const const EMPTY_TOOLS_ARRAY = [] as const
/** @public */ /** @public */
@ -133,7 +132,7 @@ export const TldrawEditor = memo(function TldrawEditor({
// defaults applied in @tldraw/tldraw. // defaults applied in @tldraw/tldraw.
const withDefaults = { const withDefaults = {
...rest, ...rest,
shapes: rest.shapes ?? EMPTY_SHAPES_ARRAY, shapeUtils: rest.shapeUtils ?? EMPTY_SHAPE_UTILS_ARRAY,
tools: rest.tools ?? EMPTY_TOOLS_ARRAY, tools: rest.tools ?? EMPTY_TOOLS_ARRAY,
} }
@ -167,12 +166,12 @@ export const TldrawEditor = memo(function TldrawEditor({
}) })
function TldrawEditorWithOwnStore( function TldrawEditorWithOwnStore(
props: Required<TldrawEditorProps & { store: undefined; user: TLUser }, 'shapes' | 'tools'> props: Required<TldrawEditorProps & { store: undefined; user: TLUser }, 'shapeUtils' | 'tools'>
) { ) {
const { defaultName, initialData, shapes, persistenceKey, sessionId, user } = props const { defaultName, initialData, shapeUtils, persistenceKey, sessionId, user } = props
const syncedStore = useLocalStore({ const syncedStore = useLocalStore({
shapes, shapeUtils,
initialData, initialData,
persistenceKey, persistenceKey,
sessionId, sessionId,
@ -186,7 +185,10 @@ const TldrawEditorWithLoadingStore = memo(function TldrawEditorBeforeLoading({
store, store,
user, user,
...rest ...rest
}: Required<TldrawEditorProps & { store: TLStoreWithStatus; user: TLUser }, 'shapes' | 'tools'>) { }: Required<
TldrawEditorProps & { store: TLStoreWithStatus; user: TLUser },
'shapeUtils' | 'tools'
>) {
const container = useContainer() const container = useContainer()
useLayoutEffect(() => { useLayoutEffect(() => {
@ -225,16 +227,16 @@ function TldrawEditorWithReadyStore({
children, children,
store, store,
tools, tools,
shapes, shapeUtils,
autoFocus, autoFocus,
user, user,
assetUrls, initialState,
}: Required< }: Required<
TldrawEditorProps & { TldrawEditorProps & {
store: TLStore store: TLStore
user: TLUser user: TLUser
}, },
'shapes' | 'tools' 'shapeUtils' | 'tools'
>) { >) {
const { ErrorFallback } = useEditorComponents() const { ErrorFallback } = useEditorComponents()
const container = useContainer() const container = useContainer()
@ -243,10 +245,11 @@ function TldrawEditorWithReadyStore({
useLayoutEffect(() => { useLayoutEffect(() => {
const editor = new Editor({ const editor = new Editor({
store, store,
shapes, shapeUtils,
tools, tools,
getContainer: () => container, getContainer: () => container,
user, user,
initialState,
}) })
;(window as any).app = editor ;(window as any).app = editor
;(window as any).editor = editor ;(window as any).editor = editor
@ -255,10 +258,10 @@ function TldrawEditorWithReadyStore({
return () => { return () => {
editor.dispose() editor.dispose()
} }
}, [container, shapes, tools, store, user]) }, [container, shapeUtils, tools, store, user, initialState])
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
if (editor && autoFocus) editor.focus() if (editor && autoFocus) editor.isFocused = true
}, [editor, autoFocus]) }, [editor, autoFocus])
const onMountEvent = useEvent((editor: Editor) => { const onMountEvent = useEvent((editor: Editor) => {
@ -288,17 +291,6 @@ function TldrawEditorWithReadyStore({
() => editor?.crashingError ?? null () => editor?.crashingError ?? null
) )
const assets = useDefaultEditorAssetsWithOverrides(assetUrls)
const { done: preloadingComplete, error: preloadingError } = usePreloadAssets(assets)
if (preloadingError) {
return <ErrorScreen>Could not load assets. Please refresh the page.</ErrorScreen>
}
if (!preloadingComplete) {
return <LoadingScreen>Loading assets...</LoadingScreen>
}
if (!editor) { if (!editor) {
return null return null
} }
@ -311,7 +303,7 @@ function TldrawEditorWithReadyStore({
// document in the event of an error to reassure them that their work is // document in the event of an error to reassure them that their work is
// not lost. // not lost.
<OptionalErrorBoundary <OptionalErrorBoundary
fallback={ErrorFallback} fallback={ErrorFallback as any}
onError={(error) => onError={(error) =>
editor.annotateError(error, { origin: 'react.tldraw', willCrashApp: true }) editor.annotateError(error, { origin: 'react.tldraw', willCrashApp: true })
} }
@ -334,7 +326,7 @@ function Layout({ children }: { children: any }) {
useSafariFocusOutFix() useSafariFocusOutFix()
useForceUpdate() useForceUpdate()
return children return children ?? <Canvas />
} }
function Crash({ crashingError }: { crashingError: unknown }): null { function Crash({ crashingError }: { crashingError: unknown }): null {

View file

@ -1,4 +1,3 @@
import { Matrix2d, toDomPrecision } from '@tldraw/primitives'
import { react, track, useQuickReactor, useValue } from '@tldraw/state' import { react, track, useQuickReactor, useValue } from '@tldraw/state'
import { TLHandle, TLShapeId } from '@tldraw/tlschema' import { TLHandle, TLShapeId } from '@tldraw/tlschema'
import { dedupe, modulate, objectMapValues } from '@tldraw/utils' import { dedupe, modulate, objectMapValues } from '@tldraw/utils'
@ -12,10 +11,10 @@ import { useFixSafariDoubleTapZoomPencilEvents } from '../hooks/useFixSafariDoub
import { useGestureEvents } from '../hooks/useGestureEvents' import { useGestureEvents } from '../hooks/useGestureEvents'
import { useHandleEvents } from '../hooks/useHandleEvents' import { useHandleEvents } from '../hooks/useHandleEvents'
import { useScreenBounds } from '../hooks/useScreenBounds' import { useScreenBounds } from '../hooks/useScreenBounds'
import { Matrix2d } from '../primitives/Matrix2d'
import { toDomPrecision } from '../primitives/utils'
import { debugFlags } from '../utils/debug-flags' import { debugFlags } from '../utils/debug-flags'
import { LiveCollaborators } from './LiveCollaborators' import { LiveCollaborators } from './LiveCollaborators'
import { SelectionBg } from './SelectionBg'
import { SelectionFg } from './SelectionFg'
import { Shape } from './Shape' import { Shape } from './Shape'
import { ShapeIndicator } from './ShapeIndicator' import { ShapeIndicator } from './ShapeIndicator'
@ -97,7 +96,7 @@ export const Canvas = track(function Canvas() {
{SvgDefs && <SvgDefs />} {SvgDefs && <SvgDefs />}
</defs> </defs>
</svg> </svg>
<SelectionBg /> <SelectionBackgroundWrapper />
<div className="tl-shapes"> <div className="tl-shapes">
<ShapesToDisplay /> <ShapesToDisplay />
</div> </div>
@ -110,7 +109,7 @@ export const Canvas = track(function Canvas() {
<HoveredShapeIndicator /> <HoveredShapeIndicator />
<HintedShapeIndicator /> <HintedShapeIndicator />
<SnapLinesWrapper /> <SnapLinesWrapper />
<SelectionFg /> <SelectionForegroundWrapper />
<LiveCollaborators /> <LiveCollaborators />
</div> </div>
</div> </div>
@ -124,12 +123,13 @@ const GridWrapper = track(function GridWrapper() {
// get grid from context // get grid from context
const { gridSize } = editor.documentSettings
const { x, y, z } = editor.camera const { x, y, z } = editor.camera
const isGridMode = editor.isGridMode const isGridMode = editor.isGridMode
if (!(Grid && isGridMode)) return null if (!(Grid && isGridMode)) return null
return <Grid x={x} y={y} z={z} size={editor.gridSize} /> return <Grid x={x} y={y} z={z} size={gridSize} />
}) })
const ScribbleWrapper = track(function ScribbleWrapper() { const ScribbleWrapper = track(function ScribbleWrapper() {
@ -432,3 +432,15 @@ const UiLogger = track(() => {
</div> </div>
) )
}) })
export function SelectionForegroundWrapper() {
const { SelectionForeground } = useEditorComponents()
if (!SelectionForeground) return null
return <SelectionForeground />
}
export function SelectionBackgroundWrapper() {
const { SelectionBackground } = useEditorComponents()
if (!SelectionBackground) return null
return <SelectionBackground />
}

View file

@ -1,6 +0,0 @@
/** @public */
export type TLBackgroundComponent = () => JSX.Element | null
export function DefaultBackground() {
return <div className="tl-background" />
}

View file

@ -1,11 +1,11 @@
import * as React from 'react' import * as React from 'react'
import { TLErrorFallbackComponent } from './DefaultErrorFallback' import { TLErrorFallbackComponent } from './default-components/DefaultErrorFallback'
/** @public */ /** @public */
export interface TLErrorBoundaryProps { export interface TLErrorBoundaryProps {
children: React.ReactNode children: React.ReactNode
onError?: ((error: unknown) => void) | null onError?: ((error: unknown) => void) | null
fallback: (props: { error: unknown }) => any fallback: TLErrorFallbackComponent
} }
type TLErrorBoundaryState = { error: Error | null } type TLErrorBoundaryState = { error: Error | null }
@ -21,13 +21,13 @@ export class ErrorBoundary extends React.Component<
return { error } return { error }
} }
state = initialState override state = initialState
componentDidCatch(error: unknown) { override componentDidCatch(error: unknown) {
this.props.onError?.(error) this.props.onError?.(error)
} }
render() { override render() {
const { error } = this.state const { error } = this.state
if (error !== null) { if (error !== null) {
@ -52,7 +52,7 @@ export function OptionalErrorBoundary({
} }
return ( return (
<ErrorBoundary fallback={fallback} {...props}> <ErrorBoundary fallback={fallback as any} {...props}>
{children} {children}
</ErrorBoundary> </ErrorBoundary>
) )

View file

@ -0,0 +1,30 @@
import { track } from '@tldraw/state'
import classNames from 'classnames'
import { HTMLProps, useLayoutEffect, useRef } from 'react'
import { useEditor } from '../hooks/useEditor'
/** @public */
export const PositionedOnCanvas = track(function PositionedOnCanvas({
x: offsetX = 0,
y: offsetY = 0,
rotation = 0,
...rest
}: {
x?: number
y?: number
rotation?: number
} & HTMLProps<HTMLDivElement>) {
const editor = useEditor()
const rContainer = useRef<HTMLDivElement>(null)
useLayoutEffect(() => {
const { x, y, z } = editor.camera
const elm = rContainer.current
if (!elm) return
if (x === undefined) return
elm.style.transform = `translate(${x}px, ${y}px) scale(${z}) rotate(${rotation}rad) translate(${offsetX}px, ${offsetY}px)`
}, [editor.camera, offsetX, offsetY, rotation])
return <div ref={rContainer} {...rest} className={classNames('tl-positioned', rest.className)} />
})

View file

@ -1,135 +0,0 @@
import { Matrix2d, toDomPrecision } from '@tldraw/primitives'
import { track } from '@tldraw/state'
import * as React from 'react'
import { TLPointerEventInfo } from '../editor/types/event-types'
import { useEditor } from '../hooks/useEditor'
import { releasePointerCapture, setPointerCapture } from '../utils/dom'
import { getPointerInfo } from '../utils/svg'
export const SelectionBg = track(function SelectionBg() {
const editor = useEditor()
const events = React.useMemo(() => {
const onPointerDown = (e: React.PointerEvent) => {
if ((e as any).isKilled) return
setPointerCapture(e.currentTarget, e)
const info: TLPointerEventInfo = {
type: 'pointer',
target: 'selection',
name: 'pointer_down',
...getPointerInfo(e, editor.getContainer()),
}
editor.dispatch(info)
}
const onPointerMove = (e: React.PointerEvent) => {
if ((e as any).isKilled) return
const info: TLPointerEventInfo = {
type: 'pointer',
target: 'selection',
name: 'pointer_move',
...getPointerInfo(e, editor.getContainer()),
}
editor.dispatch(info)
}
const onPointerUp = (e: React.PointerEvent) => {
if ((e as any).isKilled) return
releasePointerCapture(e.currentTarget, e)
const info: TLPointerEventInfo = {
type: 'pointer',
target: 'selection',
name: 'pointer_up',
...getPointerInfo(e, editor.getContainer()),
}
editor.dispatch(info)
}
const onPointerEnter = (e: React.PointerEvent) => {
if ((e as any).isKilled) return
const info: TLPointerEventInfo = {
type: 'pointer',
target: 'selection',
name: 'pointer_enter',
...getPointerInfo(e, editor.getContainer()),
}
editor.dispatch(info)
}
const onPointerLeave = (e: React.PointerEvent) => {
if ((e as any).isKilled) return
const info: TLPointerEventInfo = {
type: 'pointer',
target: 'selection',
name: 'pointer_leave',
...getPointerInfo(e, editor.getContainer()),
}
editor.dispatch(info)
}
return {
onPointerDown,
onPointerMove,
onPointerUp,
onPointerEnter,
onPointerLeave,
}
}, [editor])
const { selectionBounds: bounds, selectedIds } = editor
if (!bounds) return null
const shouldDisplay = editor.isInAny(
'select.idle',
'select.brushing',
'select.scribble_brushing',
'select.pointing_shape',
'select.pointing_selection',
'text.resizing'
)
if (selectedIds.length === 1) {
const shape = editor.getShapeById(selectedIds[0])
if (!shape) {
return null
}
const util = editor.getShapeUtil(shape)
if (util.hideSelectionBoundsBg(shape)) {
return null
}
}
const transform = Matrix2d.toCssString(
Matrix2d.Compose(
Matrix2d.Translate(bounds.minX, bounds.minY),
Matrix2d.Rotate(editor.selectionRotation)
)
)
return (
<div
className="tl-selection__bg"
draggable={false}
style={{
transform,
width: toDomPrecision(Math.max(1, bounds.width)),
height: toDomPrecision(Math.max(1, bounds.height)),
pointerEvents: shouldDisplay ? 'all' : 'none',
opacity: shouldDisplay ? 1 : 0,
}}
{...events}
/>
)
})

View file

@ -1,11 +1,11 @@
import { Matrix2d } from '@tldraw/primitives'
import { track, useQuickReactor, useStateTracking } from '@tldraw/state' import { track, useQuickReactor, useStateTracking } from '@tldraw/state'
import { TLShape, TLShapeId } from '@tldraw/tlschema' import { TLShape, TLShapeId } from '@tldraw/tlschema'
import * as React from 'react' import * as React from 'react'
import { useEditor } from '../..'
import { ShapeUtil } from '../editor/shapes/ShapeUtil' import { ShapeUtil } from '../editor/shapes/ShapeUtil'
import { useEditor } from '../hooks/useEditor'
import { useEditorComponents } from '../hooks/useEditorComponents' import { useEditorComponents } from '../hooks/useEditorComponents'
import { useShapeEvents } from '../hooks/useShapeEvents' import { useShapeEvents } from '../hooks/useShapeEvents'
import { Matrix2d } from '../primitives/Matrix2d'
import { OptionalErrorBoundary } from './ErrorBoundary' import { OptionalErrorBoundary } from './ErrorBoundary'
/* /*
@ -135,7 +135,7 @@ export const Shape = track(function Shape({
{isCulled && util.canUnmount(shape) ? ( {isCulled && util.canUnmount(shape) ? (
<CulledShape shape={shape} /> <CulledShape shape={shape} />
) : ( ) : (
<OptionalErrorBoundary fallback={ShapeErrorFallback} onError={annotateError}> <OptionalErrorBoundary fallback={ShapeErrorFallback as any} onError={annotateError}>
<InnerShape shape={shape} util={util} /> <InnerShape shape={shape} util={util} />
</OptionalErrorBoundary> </OptionalErrorBoundary>
)} )}
@ -146,7 +146,7 @@ export const Shape = track(function Shape({
const InnerShape = React.memo( const InnerShape = React.memo(
function InnerShape<T extends TLShape>({ shape, util }: { shape: T; util: ShapeUtil<T> }) { function InnerShape<T extends TLShape>({ shape, util }: { shape: T; util: ShapeUtil<T> }) {
return useStateTracking('InnerShape:' + util.type, () => util.component(shape)) return useStateTracking('InnerShape:' + shape.type, () => util.component(shape))
}, },
(prev, next) => prev.shape.props === next.shape.props && prev.shape.meta === next.shape.meta (prev, next) => prev.shape.props === next.shape.props && prev.shape.meta === next.shape.meta
) )
@ -159,7 +159,7 @@ const InnerShapeBackground = React.memo(
shape: T shape: T
util: ShapeUtil<T> util: ShapeUtil<T>
}) { }) {
return useStateTracking('InnerShape:' + util.type, () => util.backgroundComponent?.(shape)) return useStateTracking('InnerShape:' + shape.type, () => util.backgroundComponent?.(shape))
}, },
(prev, next) => prev.shape.props === next.shape.props && prev.shape.meta === next.shape.meta (prev, next) => prev.shape.props === next.shape.props && prev.shape.meta === next.shape.meta
) )

View file

@ -0,0 +1,8 @@
import { ComponentType } from 'react'
/** @public */
export type TLBackgroundComponent = ComponentType<object> | null
export function DefaultBackground() {
return <div className="tl-background" />
}

View file

@ -1,15 +1,15 @@
import { toDomPrecision } from '@tldraw/primitives'
import { Box2dModel } from '@tldraw/tlschema' import { Box2dModel } from '@tldraw/tlschema'
import { useRef } from 'react' import { ComponentType, useRef } from 'react'
import { useTransform } from '../hooks/useTransform' import { useTransform } from '../../hooks/useTransform'
import { toDomPrecision } from '../../primitives/utils'
/** @public */ /** @public */
export type TLBrushComponent = (props: { export type TLBrushComponent = ComponentType<{
brush: Box2dModel brush: Box2dModel
color?: string color?: string
opacity?: number opacity?: number
className?: string className?: string
}) => any | null }>
export const DefaultBrush: TLBrushComponent = ({ brush, color, opacity }) => { export const DefaultBrush: TLBrushComponent = ({ brush, color, opacity }) => {
const rSvg = useRef<SVGSVGElement>(null) const rSvg = useRef<SVGSVGElement>(null)

View file

@ -1,17 +1,19 @@
import { Box2d, clamp, Vec2d } from '@tldraw/primitives'
import { Vec2dModel } from '@tldraw/tlschema' import { Vec2dModel } from '@tldraw/tlschema'
import classNames from 'classnames' import classNames from 'classnames'
import { useRef } from 'react' import { ComponentType, useRef } from 'react'
import { useTransform } from '../hooks/useTransform' import { useTransform } from '../../hooks/useTransform'
import { Box2d } from '../../primitives/Box2d'
import { Vec2d } from '../../primitives/Vec2d'
import { clamp } from '../../primitives/utils'
export type TLCollaboratorHintComponent = (props: { export type TLCollaboratorHintComponent = ComponentType<{
className?: string className?: string
point: Vec2dModel point: Vec2dModel
viewport: Box2d viewport: Box2d
zoom: number zoom: number
opacity?: number opacity?: number
color: string color: string
}) => JSX.Element | null }>
export const DefaultCollaboratorHint: TLCollaboratorHintComponent = ({ export const DefaultCollaboratorHint: TLCollaboratorHintComponent = ({
className, className,

View file

@ -1,17 +1,17 @@
import { Vec2dModel } from '@tldraw/tlschema' import { Vec2dModel } from '@tldraw/tlschema'
import classNames from 'classnames' import classNames from 'classnames'
import { memo, useRef } from 'react' import { ComponentType, memo, useRef } from 'react'
import { useTransform } from '../hooks/useTransform' import { useTransform } from '../../hooks/useTransform'
/** @public */ /** @public */
export type TLCursorComponent = (props: { export type TLCursorComponent = ComponentType<{
className?: string className?: string
point: Vec2dModel | null point: Vec2dModel | null
zoom: number zoom: number
color?: string color?: string
name: string | null name: string | null
chatMessage: string chatMessage: string
}) => any | null }>
const _Cursor: TLCursorComponent = ({ className, zoom, point, color, name, chatMessage }) => { const _Cursor: TLCursorComponent = ({ className, zoom, point, color, name, chatMessage }) => {
const rCursor = useRef<HTMLDivElement>(null) const rCursor = useRef<HTMLDivElement>(null)

View file

@ -1,12 +1,12 @@
import { useValue } from '@tldraw/state' import { useValue } from '@tldraw/state'
import classNames from 'classnames' import classNames from 'classnames'
import { useEffect, useLayoutEffect, useRef, useState } from 'react' import { ComponentType, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { Editor } from '../editor/Editor' import { Editor } from '../../editor/Editor'
import { EditorContext } from '../hooks/useEditor' import { EditorContext } from '../../hooks/useEditor'
import { hardResetEditor } from '../utils/hard-reset' import { hardResetEditor } from '../../utils/hardResetEditor'
import { refreshPage } from '../utils/refresh-page' import { refreshPage } from '../../utils/refreshPage'
import { Canvas } from './Canvas' import { Canvas } from '../Canvas'
import { ErrorBoundary } from './ErrorBoundary' import { ErrorBoundary } from '../ErrorBoundary'
const BASE_ERROR_URL = 'https://github.com/tldraw/tldraw/issues/new' const BASE_ERROR_URL = 'https://github.com/tldraw/tldraw/issues/new'
@ -14,7 +14,7 @@ const BASE_ERROR_URL = 'https://github.com/tldraw/tldraw/issues/new'
function noop() {} function noop() {}
/** @public */ /** @public */
export type TLErrorFallbackComponent = (props: { error: unknown; editor?: Editor }) => any | null export type TLErrorFallbackComponent = ComponentType<{ error: unknown; editor?: Editor }>
/** @internal */ /** @internal */
export const DefaultErrorFallback: TLErrorFallbackComponent = ({ error, editor }) => { export const DefaultErrorFallback: TLErrorFallbackComponent = ({ error, editor }) => {

View file

@ -1,13 +1,14 @@
import { modulate } from '@tldraw/utils' import { modulate } from '@tldraw/utils'
import { GRID_STEPS } from '../constants' import { ComponentType } from 'react'
import { GRID_STEPS } from '../../constants'
/** @public */ /** @public */
export type TLGridComponent = (props: { export type TLGridComponent = ComponentType<{
x: number x: number
y: number y: number
z: number z: number
size: number size: number
}) => JSX.Element | null }>
export const DefaultGrid: TLGridComponent = ({ x, y, z, size }) => { export const DefaultGrid: TLGridComponent = ({ x, y, z, size }) => {
return ( return (

View file

@ -1,11 +1,12 @@
import { TLHandle, TLShapeId } from '@tldraw/tlschema' import { TLHandle, TLShapeId } from '@tldraw/tlschema'
import classNames from 'classnames' import classNames from 'classnames'
import { ComponentType } from 'react'
export type TLHandleComponent = (props: { export type TLHandleComponent = ComponentType<{
shapeId: TLShapeId shapeId: TLShapeId
handle: TLHandle handle: TLHandle
className?: string className?: string
}) => any | null }>
export const DefaultHandle: TLHandleComponent = ({ handle, className }) => { export const DefaultHandle: TLHandleComponent = ({ handle, className }) => {
return ( return (

View file

@ -0,0 +1,36 @@
import { TLScribble } from '@tldraw/tlschema'
import classNames from 'classnames'
import { ComponentType } from 'react'
import { getSvgPathFromPoints } from '../../utils/getSvgPathFromPoints'
/** @public */
export type TLScribbleComponent = ComponentType<{
scribble: TLScribble
zoom: number
color?: string
opacity?: number
className?: string
}>
export const DefaultScribble: TLScribbleComponent = ({
scribble,
zoom,
color,
opacity,
className,
}) => {
if (!scribble.points.length) return null
return (
<svg className={className ? classNames('tl-overlays__item', className) : className}>
<path
className="tl-scribble"
d={getSvgPathFromPoints(scribble.points, false)}
stroke={color ?? `var(--color-${scribble.color})`}
fill="none"
strokeWidth={8 / zoom}
opacity={opacity ?? scribble.opacity}
/>
</svg>
)
}

View file

@ -0,0 +1,141 @@
import { track } from '@tldraw/state'
import * as React from 'react'
import { TLPointerEventInfo } from '../../editor/types/event-types'
import { useEditor } from '../../hooks/useEditor'
import { Matrix2d } from '../../primitives/Matrix2d'
import { toDomPrecision } from '../../primitives/utils'
import { releasePointerCapture, setPointerCapture } from '../../utils/dom'
import { getPointerInfo } from '../../utils/getPointerInfo'
/** @public */
export type TLSelectionBackgroundComponent = React.ComponentType<object>
export const DefaultSelectionBackground: TLSelectionBackgroundComponent = track(
function SelectionBg() {
const editor = useEditor()
const events = React.useMemo(() => {
const onPointerDown = (e: React.PointerEvent) => {
if ((e as any).isKilled) return
setPointerCapture(e.currentTarget, e)
const info: TLPointerEventInfo = {
type: 'pointer',
target: 'selection',
name: 'pointer_down',
...getPointerInfo(e, editor.getContainer()),
}
editor.dispatch(info)
}
const onPointerMove = (e: React.PointerEvent) => {
if ((e as any).isKilled) return
const info: TLPointerEventInfo = {
type: 'pointer',
target: 'selection',
name: 'pointer_move',
...getPointerInfo(e, editor.getContainer()),
}
editor.dispatch(info)
}
const onPointerUp = (e: React.PointerEvent) => {
if ((e as any).isKilled) return
releasePointerCapture(e.currentTarget, e)
const info: TLPointerEventInfo = {
type: 'pointer',
target: 'selection',
name: 'pointer_up',
...getPointerInfo(e, editor.getContainer()),
}
editor.dispatch(info)
}
const onPointerEnter = (e: React.PointerEvent) => {
if ((e as any).isKilled) return
const info: TLPointerEventInfo = {
type: 'pointer',
target: 'selection',
name: 'pointer_enter',
...getPointerInfo(e, editor.getContainer()),
}
editor.dispatch(info)
}
const onPointerLeave = (e: React.PointerEvent) => {
if ((e as any).isKilled) return
const info: TLPointerEventInfo = {
type: 'pointer',
target: 'selection',
name: 'pointer_leave',
...getPointerInfo(e, editor.getContainer()),
}
editor.dispatch(info)
}
return {
onPointerDown,
onPointerMove,
onPointerUp,
onPointerEnter,
onPointerLeave,
}
}, [editor])
const { selectionBounds: bounds, selectedIds } = editor
if (!bounds) return null
const shouldDisplay = editor.isInAny(
'select.idle',
'select.brushing',
'select.scribble_brushing',
'select.pointing_shape',
'select.pointing_selection',
'text.resizing'
)
if (selectedIds.length === 1) {
const shape = editor.getShapeById(selectedIds[0])
if (!shape) {
return null
}
const util = editor.getShapeUtil(shape)
if (util.hideSelectionBoundsBg(shape)) {
return null
}
}
const transform = Matrix2d.toCssString(
Matrix2d.Compose(
Matrix2d.Translate(bounds.minX, bounds.minY),
Matrix2d.Rotate(editor.selectionRotation)
)
)
return (
<div
className="tl-selection__bg"
draggable={false}
style={{
transform,
width: toDomPrecision(Math.max(1, bounds.width)),
height: toDomPrecision(Math.max(1, bounds.height)),
pointerEvents: shouldDisplay ? 'all' : 'none',
opacity: shouldDisplay ? 1 : 0,
}}
{...events}
/>
)
}
)

View file

@ -0,0 +1,49 @@
import { track } from '@tldraw/state'
import classNames from 'classnames'
import { ComponentType, useRef } from 'react'
import { useEditor } from '../../hooks/useEditor'
import { useTransform } from '../../hooks/useTransform'
import { toDomPrecision } from '../../primitives/utils'
/** @public */
export type TLSelectionForegroundComponent = ComponentType<object>
export const DefaultSelectionForeground: TLSelectionForegroundComponent = track(() => {
const editor = useEditor()
const rSvg = useRef<SVGSVGElement>(null)
let bounds = editor.selectionBounds
const onlyShape = editor.onlySelectedShape
// if all shapes have an expandBy for the selection outline, we can expand by the l
const expandOutlineBy = onlyShape
? editor.getShapeUtil(onlyShape).expandSelectionOutlinePx(onlyShape)
: 0
useTransform(rSvg, bounds?.x, bounds?.y, 1, editor.selectionRotation, {
x: -expandOutlineBy,
y: -expandOutlineBy,
})
if (!bounds) return null
bounds = bounds.clone().expandBy(expandOutlineBy)
const width = Math.max(1, bounds.width)
const height = Math.max(1, bounds.height)
return (
<svg
ref={rSvg}
className="tl-overlays__item tl-selection__fg"
data-testid="selection-foreground"
>
<rect
className={classNames('tl-selection__fg__outline')}
width={toDomPrecision(width)}
height={toDomPrecision(height)}
/>
</svg>
)
})

View file

@ -1,5 +1,7 @@
import { ComponentType } from 'react'
/** @public */ /** @public */
export type TLShapeErrorFallbackComponent = (props: { error: any }) => any | null export type TLShapeErrorFallbackComponent = ComponentType<{ error: any }>
/** @internal */ /** @internal */
export const DefaultShapeErrorFallback: TLShapeErrorFallbackComponent = ({ export const DefaultShapeErrorFallback: TLShapeErrorFallbackComponent = ({

View file

@ -1,7 +1,9 @@
import { ComponentType } from 'react'
/** @public */ /** @public */
export type TLShapeIndicatorErrorFallback = (props: { error: unknown }) => any | null export type TLShapeIndicatorErrorFallbackComponent = ComponentType<{ error: unknown }>
/** @internal */ /** @internal */
export const DefaultShapeIndicatorErrorFallback: TLShapeIndicatorErrorFallback = () => { export const DefaultShapeIndicatorErrorFallback: TLShapeIndicatorErrorFallbackComponent = () => {
return <circle cx={4} cy={4} r={8} strokeWidth="1" stroke="red" /> return <circle cx={4} cy={4} r={8} strokeWidth="1" stroke="red" />
} }

View file

@ -1,11 +1,11 @@
import { rangeIntersection } from '@tldraw/primitives'
import classNames from 'classnames' import classNames from 'classnames'
import * as React from 'react' import * as React from 'react'
import { import {
type GapsSnapLine, type GapsSnapLine,
type PointsSnapLine, type PointsSnapLine,
type SnapLine, type SnapLine,
} from '../editor/managers/SnapManager' } from '../../editor/managers/SnapManager'
import { rangeIntersection } from '../../primitives/utils'
function PointsSnapLine({ points, zoom }: { zoom: number } & PointsSnapLine) { function PointsSnapLine({ points, zoom }: { zoom: number } & PointsSnapLine) {
const l = 2.5 / zoom const l = 2.5 / zoom
@ -153,11 +153,11 @@ function GapsSnapLine({ gaps, direction, zoom }: { zoom: number } & GapsSnapLine
) )
} }
export type TLSnapLineComponent = (props: { export type TLSnapLineComponent = React.ComponentType<{
className?: string className?: string
line: SnapLine line: SnapLine
zoom: number zoom: number
}) => any }>
export const DefaultSnapLine: TLSnapLineComponent = ({ className, line, zoom }) => { export const DefaultSnapLine: TLSnapLineComponent = ({ className, line, zoom }) => {
return ( return (

View file

@ -1,4 +1,6 @@
export type TLSpinnerComponent = () => any | null import { ComponentType } from 'react'
export type TLSpinnerComponent = ComponentType<object>
export const DefaultSpinner: TLSpinnerComponent = () => { export const DefaultSpinner: TLSpinnerComponent = () => {
return ( return (

View file

@ -1,120 +0,0 @@
import { StrokePoint, toDomPrecision, Vec2d, VecLike } from '@tldraw/primitives'
export function getPointerInfo(e: React.PointerEvent | PointerEvent) {
;(e as any).isKilled = true
return {
point: {
x: e.clientX,
y: e.clientY,
z: e.pressure,
},
shiftKey: e.shiftKey,
altKey: e.altKey,
ctrlKey: e.metaKey || e.ctrlKey,
pointerId: e.pointerId,
button: e.button,
isPen: e.pointerType === 'pen',
}
}
function precise(A: VecLike) {
return `${toDomPrecision(A.x)},${toDomPrecision(A.y)} `
}
function average(A: VecLike, B: VecLike) {
return `${toDomPrecision((A.x + B.x) / 2)},${toDomPrecision((A.y + B.y) / 2)} `
}
/**
* Turn an array of points into a path of quadradic curves.
*
* @param points - The points returned from perfect-freehand
* @param closed - Whether the stroke is closed
*/
export function getSvgPathFromStroke(points: Vec2d[], closed = true): string {
const len = points.length
if (len < 2) {
return ''
}
let a = points[0]
let b = points[1]
if (len === 2) {
// If only two points, just draw a line
return `M${precise(a)}L${precise(b)}`
}
let result = ''
for (let i = 2, max = len - 1; i < max; i++) {
a = points[i]
b = points[i + 1]
result += average(a, b)
}
if (closed) {
// If closed, draw a curve from the last point to the first
return `M${average(points[0], points[1])}Q${precise(points[1])}${average(
points[1],
points[2]
)}T${result}${average(points[len - 1], points[0])}${average(points[0], points[1])}Z`
} else {
// If not closed, draw a curve starting at the first point and
// ending at the midpoint of the last and second-last point, then
// complete the curve with a line segment to the last point.
return `M${precise(points[0])}Q${precise(points[1])}${average(points[1], points[2])}${
points.length > 3 ? 'T' : ''
}${result}L${precise(points[len - 1])}`
}
}
/**
* Turn an array of stroke points into a path of quadradic curves.
*
* @param points - The stroke points returned from perfect-freehand
* @param closed - Whether the shape is closed
*/
export function getSvgPathFromStrokePoints(points: StrokePoint[], closed = false): string {
const len = points.length
if (len < 2) {
return ''
}
let a = points[0].point
let b = points[1].point
if (len === 2) {
return `M${precise(a)}L${precise(b)}`
}
let result = ''
for (let i = 2, max = len - 1; i < max; i++) {
a = points[i].point
b = points[i + 1].point
result += average(a, b)
}
if (closed) {
// If closed, draw a curve from the last point to the first
return `M${average(points[0].point, points[1].point)}Q${precise(points[1].point)}${average(
points[1].point,
points[2].point
)}T${result}${average(points[len - 1].point, points[0].point)}${average(
points[0].point,
points[1].point
)}Z`
} else {
// If not closed, draw a curve starting at the first point and
// ending at the midpoint of the last and second-last point, then
// complete the curve with a line segment to the last point.
return `M${precise(points[0].point)}Q${precise(points[1].point)}${average(
points[1].point,
points[2].point
)}${points.length > 3 ? 'T' : ''}${result}L${precise(points[len - 1].point)}`
}
}

View file

@ -19,7 +19,7 @@ import {
} from '@tldraw/tlschema' } from '@tldraw/tlschema'
import { objectMapFromEntries } from '@tldraw/utils' import { objectMapFromEntries } from '@tldraw/utils'
import { T } from '@tldraw/validate' import { T } from '@tldraw/validate'
import { uniqueId } from '../utils/data' import { uniqueId } from '../utils/uniqueId'
const tabIdKey = 'TLDRAW_TAB_ID_v2' as const const tabIdKey = 'TLDRAW_TAB_ID_v2' as const

View file

@ -2,7 +2,7 @@ import { atom } from '@tldraw/state'
import { defineMigrations, migrate } from '@tldraw/store' import { defineMigrations, migrate } from '@tldraw/store'
import { getDefaultTranslationLocale } from '@tldraw/tlschema' import { getDefaultTranslationLocale } from '@tldraw/tlschema'
import { T } from '@tldraw/validate' import { T } from '@tldraw/validate'
import { uniqueId } from '../utils/data' import { uniqueId } from '../utils/uniqueId'
const USER_DATA_KEY = 'TLDRAW_USER_DATA_v3' const USER_DATA_KEY = 'TLDRAW_USER_DATA_v3'

View file

@ -1,13 +1,23 @@
import { HistoryEntry, SerializedStore, Store, StoreSchema } from '@tldraw/store' import { HistoryEntry, SerializedStore, Store, StoreSchema } from '@tldraw/store'
import { TLRecord, TLStore, TLStoreProps, createTLSchema } from '@tldraw/tlschema' import {
import { checkShapesAndAddCore } from './defaultShapes' SchemaShapeInfo,
import { AnyTLShapeInfo, TLShapeInfo } from './defineShape' TLRecord,
TLStore,
TLStoreProps,
TLUnknownShape,
createTLSchema,
} from '@tldraw/tlschema'
import { TLShapeUtilConstructor } from '../editor/shapes/ShapeUtil'
import { TLAnyShapeUtilConstructor, checkShapesAndAddCore } from './defaultShapes'
/** @public */ /** @public */
export type TLStoreOptions = { export type TLStoreOptions = {
initialData?: SerializedStore<TLRecord> initialData?: SerializedStore<TLRecord>
defaultName?: string defaultName?: string
} & ({ shapes: readonly AnyTLShapeInfo[] } | { schema: StoreSchema<TLRecord, TLStoreProps> }) } & (
| { shapeUtils: readonly TLAnyShapeUtilConstructor[] }
| { schema: StoreSchema<TLRecord, TLStoreProps> }
)
/** @public */ /** @public */
export type TLStoreEventInfo = HistoryEntry<TLRecord> export type TLStoreEventInfo = HistoryEntry<TLRecord>
@ -22,7 +32,7 @@ export function createTLStore({ initialData, defaultName = '', ...rest }: TLStor
const schema = const schema =
'schema' in rest 'schema' in rest
? rest.schema ? rest.schema
: createTLSchema({ shapes: shapesArrayToShapeMap(checkShapesAndAddCore(rest.shapes)) }) : createTLSchema({ shapes: shapesArrayToShapeMap(checkShapesAndAddCore(rest.shapeUtils)) })
return new Store({ return new Store({
schema, schema,
initialData, initialData,
@ -32,6 +42,14 @@ export function createTLStore({ initialData, defaultName = '', ...rest }: TLStor
}) })
} }
function shapesArrayToShapeMap(shapes: TLShapeInfo[]) { function shapesArrayToShapeMap(shapeUtils: TLShapeUtilConstructor<TLUnknownShape>[]) {
return Object.fromEntries(shapes.map((s) => [s.type, s])) return Object.fromEntries(
shapeUtils.map((s): [string, SchemaShapeInfo] => [
s.type,
{
props: s.props,
migrations: s.migrations,
},
])
)
} }

View file

@ -1,47 +1,19 @@
import { ArrowShape } from '../editor/shapes/arrow/ArrowShape' import { TLShapeUtilConstructor } from '../editor/shapes/ShapeUtil'
import { BookmarkShape } from '../editor/shapes/bookmark/BookmarkShape' import { GroupShapeUtil } from '../editor/shapes/group/GroupShapeUtil'
import { DrawShape } from '../editor/shapes/draw/DrawShape'
import { EmbedShape } from '../editor/shapes/embed/EmbedShape' /** @public */
import { FrameShape } from '../editor/shapes/frame/FrameShape' export type TLAnyShapeUtilConstructor = TLShapeUtilConstructor<any>
import { GeoShape } from '../editor/shapes/geo/GeoShape'
import { GroupShape } from '../editor/shapes/group/GroupShape'
import { HighlightShape } from '../editor/shapes/highlight/HighlightShape'
import { ImageShape } from '../editor/shapes/image/ImageShape'
import { LineShape } from '../editor/shapes/line/LineShape'
import { NoteShape } from '../editor/shapes/note/NoteShape'
import { TextShape } from '../editor/shapes/text/TextShape'
import { VideoShape } from '../editor/shapes/video/VideoShape'
import { AnyTLShapeInfo, TLShapeInfo } from './defineShape'
/** @public */ /** @public */
export const coreShapes = [ export const coreShapes = [
// created by grouping interactions, probably the corest core shape that we have // created by grouping interactions, probably the corest core shape that we have
GroupShape, GroupShapeUtil,
// created by embed menu / url drop
EmbedShape,
// created by copy and paste / url drop
BookmarkShape,
// created by copy and paste / file drop
ImageShape,
// created by copy and paste
TextShape,
] as const
/** @public */
export const defaultShapes = [
DrawShape,
GeoShape,
LineShape,
NoteShape,
FrameShape,
ArrowShape,
HighlightShape,
VideoShape,
] as const ] as const
const coreShapeTypes = new Set<string>(coreShapes.map((s) => s.type)) const coreShapeTypes = new Set<string>(coreShapes.map((s) => s.type))
export function checkShapesAndAddCore(customShapes: readonly TLShapeInfo[]) {
const shapes: AnyTLShapeInfo[] = [...coreShapes] export function checkShapesAndAddCore(customShapes: readonly TLAnyShapeUtilConstructor[]) {
const shapes = [...coreShapes] as TLAnyShapeUtilConstructor[]
const addedCustomShapeTypes = new Set<string>() const addedCustomShapeTypes = new Set<string>()
for (const customShape of customShapes) { for (const customShape of customShapes) {

View file

@ -1,32 +0,0 @@
import { ArrowShapeTool } from '../editor/shapes/arrow/ArrowShapeTool'
import { DrawShapeTool } from '../editor/shapes/draw/DrawShapeTool'
import { FrameShapeTool } from '../editor/shapes/frame/FrameShapeTool'
import { GeoShapeTool } from '../editor/shapes/geo/GeoShapeTool'
import { HighlightShapeTool } from '../editor/shapes/highlight/HighlightShapeTool'
import { LineShapeTool } from '../editor/shapes/line/LineShapeTool'
import { NoteShapeTool } from '../editor/shapes/note/NoteShapeTool'
import { TextShapeTool } from '../editor/shapes/text/TextShapeTool'
import { EraserTool } from '../editor/tools/EraserTool/EraserTool'
import { HandTool } from '../editor/tools/HandTool/HandTool'
import { LaserTool } from '../editor/tools/LaserTool/LaserTool'
import { TLStateNodeConstructor } from '../editor/tools/StateNode'
/** @public */
export const coreTools = [
// created by copy and paste
TextShapeTool,
]
/** @public */
export const defaultTools: TLStateNodeConstructor[] = [
HandTool,
EraserTool,
LaserTool,
DrawShapeTool,
GeoShapeTool,
LineShapeTool,
NoteShapeTool,
FrameShapeTool,
ArrowShapeTool,
HighlightShapeTool,
]

View file

@ -1,26 +0,0 @@
import { Migrations } from '@tldraw/store'
import { ShapeProps, TLBaseShape, TLUnknownShape } from '@tldraw/tlschema'
import { assert } from '@tldraw/utils'
import { TLShapeUtilConstructor } from '../editor/shapes/ShapeUtil'
/** @public */
export type TLShapeInfo<T extends TLUnknownShape = TLUnknownShape> = {
type: T['type']
util: TLShapeUtilConstructor<T>
props?: ShapeProps<T>
migrations?: Migrations
}
export type AnyTLShapeInfo = TLShapeInfo<TLBaseShape<any, any>>
/** @public */
export function defineShape<T extends TLUnknownShape>(
type: T['type'],
opts: Omit<TLShapeInfo<T>, 'type'>
): TLShapeInfo<T> {
assert(
type === opts.util.type,
`Shape type "${type}" does not match util type "${opts.util.type}"`
)
return { type, ...opts }
}

View file

@ -1,21 +1,10 @@
import { EASINGS } from '@tldraw/primitives' import { EASINGS } from './primitives/easings'
/** @internal */ /** @internal */
export const MAX_SHAPES_PER_PAGE = 2000 export const MAX_SHAPES_PER_PAGE = 2000
/** @internal */ /** @internal */
export const MAX_PAGES = 40 export const MAX_PAGES = 40
/** @internal */
export const REMOVE_SYMBOL = Symbol('remove')
/** @internal */
export const RICH_TYPES: Record<string, boolean> = {
Date: true,
RegExp: true,
String: true,
Number: true,
}
/** @internal */ /** @internal */
export const ANIMATION_SHORT_MS = 80 export const ANIMATION_SHORT_MS = 80
/** @internal */ /** @internal */
@ -44,17 +33,9 @@ export const MAJOR_NUDGE_FACTOR = 10
/** @internal */ /** @internal */
export const MINOR_NUDGE_FACTOR = 1 export const MINOR_NUDGE_FACTOR = 1
/** @internal */
export const MAX_ASSET_WIDTH = 1000
/** @internal */
export const MAX_ASSET_HEIGHT = 1000
/** @internal */ /** @internal */
export const GRID_INCREMENT = 5 export const GRID_INCREMENT = 5
/** @internal */
export const MIN_CROP_SIZE = 8
/** @internal */ /** @internal */
export const DOUBLE_CLICK_DURATION = 450 export const DOUBLE_CLICK_DURATION = 450
/** @internal */ /** @internal */
@ -84,7 +65,7 @@ export const DEFAULT_ANIMATION_OPTIONS = {
} }
/** @internal */ /** @internal */
export const HAND_TOOL_FRICTION = 0.09 export const CAMERA_SLIDE_FRICTION = 0.09
/** @public */ /** @public */
export const GRID_STEPS = [ export const GRID_STEPS = [

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,11 @@
import { Vec2d } from '@tldraw/primitives'
import { import {
COARSE_DRAG_DISTANCE, COARSE_DRAG_DISTANCE,
DOUBLE_CLICK_DURATION, DOUBLE_CLICK_DURATION,
DRAG_DISTANCE, DRAG_DISTANCE,
MULTI_CLICK_DURATION, MULTI_CLICK_DURATION,
} from '../../constants' } from '../../constants'
import { uniqueId } from '../../utils/data' import { Vec2d } from '../../primitives/Vec2d'
import { uniqueId } from '../../utils/uniqueId'
import type { Editor } from '../Editor' import type { Editor } from '../Editor'
import { TLClickEventInfo, TLPointerEventInfo } from '../types/event-types' import { TLClickEventInfo, TLPointerEventInfo } from '../types/event-types'

View file

@ -1,599 +0,0 @@
import { Vec2d, VecLike } from '@tldraw/primitives'
import {
AssetRecordType,
EmbedDefinition,
TLAsset,
TLAssetId,
TLEmbedShape,
TLShapePartial,
TLTextShape,
TLTextShapeProps,
createShapeId,
} from '@tldraw/tlschema'
import { compact, getHashForString } from '@tldraw/utils'
import { MAX_ASSET_HEIGHT, MAX_ASSET_WIDTH } from '../../constants'
import {
ACCEPTED_IMG_TYPE,
ACCEPTED_VID_TYPE,
containBoxSize,
getFileMetaData,
getImageSizeFromSrc,
getResizedImageDataUrl,
getVideoSizeFromSrc,
isImage,
} from '../../utils/assets'
import { truncateStringWithEllipsis } from '../../utils/dom'
import { getEmbedInfo } from '../../utils/embeds'
import { Editor } from '../Editor'
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../shapes/shared/default-shape-constants'
import { INDENT } from '../shapes/text/TextHelpers'
/** @public */
export type TLExternalContent =
| {
type: 'text'
point?: VecLike
text: string
}
| {
type: 'files'
files: File[]
point?: VecLike
ignoreParent: boolean
}
| {
type: 'url'
url: string
point?: VecLike
}
| {
type: 'svg-text'
text: string
point?: VecLike
}
| {
type: 'embed'
url: string
point?: VecLike
embed: EmbedDefinition
}
/** @public */
export class ExternalContentManager {
constructor(public editor: Editor) {}
handleContent = async (info: TLExternalContent) => {
switch (info.type) {
case 'text': {
return await this.handleText(this.editor, info)
}
case 'files': {
return await this.handleFiles(this.editor, info)
}
case 'embed': {
return await this.handleEmbed(this.editor, info)
}
case 'svg-text': {
return await this.handleSvgText(this.editor, info)
}
case 'url': {
return await this.handleUrl(this.editor, info)
}
}
}
/**
* Handle svg text from an external source. Feeling lucky? Overwrite this at runtime to change the way this type of external content is handled.
*
* @example
* ```ts
* editor.this.handleSvgText = myCustomMethod
* ```
*
* @param editor - The editor instance.
* @param info - The info object describing the external content.
*
* @public
*/
async handleSvgText(
editor: Editor,
{ point, text }: Extract<TLExternalContent, { type: 'svg-text' }>
) {
const position =
point ?? (editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.viewportPageCenter)
const svg = new DOMParser().parseFromString(text, 'image/svg+xml').querySelector('svg')
if (!svg) {
throw new Error('No <svg/> element present')
}
let width = parseFloat(svg.getAttribute('width') || '0')
let height = parseFloat(svg.getAttribute('height') || '0')
if (!(width && height)) {
document.body.appendChild(svg)
const box = svg.getBoundingClientRect()
document.body.removeChild(svg)
width = box.width
height = box.height
}
const asset = await this.createAssetFromFile(
editor,
new File([text], 'asset.svg', { type: 'image/svg+xml' })
)
this.createShapesForAssets(editor, [asset], position)
}
/**
* Handle embed info from an external source. Feeling lucky? Overwrite this at runtime to change the way this type of external content is handled.
*
* @example
* ```ts
* editor.this.handleEmbed = myCustomMethod
* ```
*
* @param editor - The editor instance
* @param info - The info object describing the external content.
*
* @public
*/
async handleEmbed(
editor: Editor,
{ point, url, embed }: Extract<TLExternalContent, { type: 'embed' }>
) {
const position =
point ?? (editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.viewportPageCenter)
const { width, height } = embed
const shapePartial: TLShapePartial<TLEmbedShape> = {
id: createShapeId(),
type: 'embed',
x: position.x - (width || 450) / 2,
y: position.y - (height || 450) / 2,
props: {
w: width,
h: height,
url,
},
}
editor.createShapes([shapePartial], true)
}
/**
* Handle files from an external source. Feeling lucky? Overwrite this at runtime to change the way this type of external content is handled.
*
* @example
* ```ts
* editor.this.handleFiles = myCustomMethod
* ```
*
* @param editor - The editor instance
* @param info - The info object describing the external content.
*
* @public
*/
async handleFiles(
editor: Editor,
{ point, files }: Extract<TLExternalContent, { type: 'files' }>
) {
const position =
point ?? (editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.viewportPageCenter)
const pagePoint = new Vec2d(position.x, position.y)
const assets: TLAsset[] = []
await Promise.all(
files.map(async (file, i) => {
// Use mime type instead of file ext, this is because
// window.navigator.clipboard does not preserve file names
// of copied files.
if (!file.type) throw new Error('No mime type')
// We can only accept certain extensions (either images or a videos)
if (!ACCEPTED_IMG_TYPE.concat(ACCEPTED_VID_TYPE).includes(file.type)) {
console.warn(`${file.name} not loaded - Extension not allowed.`)
return null
}
try {
const asset = await this.createAssetFromFile(editor, file)
if (!asset) throw Error('Could not create an asset')
assets[i] = asset
} catch (error) {
console.error(error)
return null
}
})
)
this.createShapesForAssets(editor, compact(assets), pagePoint)
}
/**
* Handle plain text from an external source. Feeling lucky? Overwrite this at runtime to change the way this type of external content is handled.
*
* @example
* ```ts
* editor.this.handleText = myCustomMethod
* ```
*
* @param editor - The editor instance
* @param info - The info object describing the external content.
*
* @public
*/
async handleText(editor: Editor, { point, text }: Extract<TLExternalContent, { type: 'text' }>) {
const p =
point ?? (editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.viewportPageCenter)
const defaultProps = editor.getShapeUtil<TLTextShape>('text').getDefaultProps()
const textToPaste = stripTrailingWhitespace(
stripCommonMinimumIndentation(replaceTabsWithSpaces(text))
)
// Measure the text with default values
let w: number
let h: number
let autoSize: boolean
let align = 'middle' as TLTextShapeProps['align']
const isMultiLine = textToPaste.split('\n').length > 1
// check whether the text contains the most common characters in RTL languages
const isRtl = rtlRegex.test(textToPaste)
if (isMultiLine) {
align = isMultiLine ? (isRtl ? 'end' : 'start') : 'middle'
}
const rawSize = editor.textMeasure.measureText(textToPaste, {
...TEXT_PROPS,
fontFamily: FONT_FAMILIES[defaultProps.font],
fontSize: FONT_SIZES[defaultProps.size],
width: 'fit-content',
})
const minWidth = Math.min(
isMultiLine ? editor.viewportPageBounds.width * 0.9 : 920,
Math.max(200, editor.viewportPageBounds.width * 0.9)
)
if (rawSize.w > minWidth) {
const shrunkSize = editor.textMeasure.measureText(textToPaste, {
...TEXT_PROPS,
fontFamily: FONT_FAMILIES[defaultProps.font],
fontSize: FONT_SIZES[defaultProps.size],
width: minWidth + 'px',
})
w = shrunkSize.w
h = shrunkSize.h
autoSize = false
align = isRtl ? 'end' : 'start'
} else {
// autosize is fine
w = rawSize.w
h = rawSize.h
autoSize = true
}
if (p.y - h / 2 < editor.viewportPageBounds.minY + 40) {
p.y = editor.viewportPageBounds.minY + 40 + h / 2
}
editor.createShapes<TLTextShape>([
{
id: createShapeId(),
type: 'text',
x: p.x - w / 2,
y: p.y - h / 2,
props: {
text: textToPaste,
// if the text has more than one line, align it to the left
align,
autoSize,
w,
},
},
])
}
/**
* Handle urls from an external source. Feeling lucky? Overwrite this at runtime to change the way this type of external content is handled.
*
* @example
* ```ts
* editor.this.handleUrl = myCustomMethod
* ```
*
* @param editor - The editor instance
* @param info - The info object describing the external content.
*
* @public
*/
handleUrl = async (
editor: Editor,
{ point, url }: Extract<TLExternalContent, { type: 'url' }>
) => {
// try to paste as an embed first
const embedInfo = getEmbedInfo(url)
if (embedInfo) {
return this.handleEmbed(editor, {
type: 'embed',
url: embedInfo.url,
point,
embed: embedInfo.definition,
})
}
const position =
point ?? (editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.viewportPageCenter)
const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url))
// Use an existing asset if we have one, or else else create a new one
let asset = editor.getAssetById(assetId) as TLAsset
let shouldAlsoCreateAsset = false
if (!asset) {
shouldAlsoCreateAsset = true
asset = await this.createAssetFromUrl(editor, url)
}
editor.batch(() => {
if (shouldAlsoCreateAsset) {
editor.createAssets([asset])
}
this.createShapesForAssets(editor, [asset], position)
})
}
async createShapesForAssets(editor: Editor, assets: TLAsset[], position: VecLike) {
if (!assets.length) return
const currentPoint = Vec2d.From(position)
const paritals: TLShapePartial[] = []
for (const asset of assets) {
switch (asset.type) {
case 'bookmark': {
paritals.push({
id: createShapeId(),
type: 'bookmark',
x: currentPoint.x - 150,
y: currentPoint.y - 160,
opacity: 1,
props: {
assetId: asset.id,
url: asset.props.src,
},
})
currentPoint.x += 300
break
}
case 'image': {
paritals.push({
id: createShapeId(),
type: 'image',
x: currentPoint.x - asset.props.w / 2,
y: currentPoint.y - asset.props.h / 2,
opacity: 1,
props: {
assetId: asset.id,
w: asset.props.w,
h: asset.props.h,
},
})
currentPoint.x += asset.props.w
break
}
case 'video': {
paritals.push({
id: createShapeId(),
type: 'video',
x: currentPoint.x - asset.props.w / 2,
y: currentPoint.y - asset.props.h / 2,
opacity: 1,
props: {
assetId: asset.id,
w: asset.props.w,
h: asset.props.h,
},
})
currentPoint.x += asset.props.w
}
}
}
editor.batch(() => {
// Create any assets
const assetsToCreate = assets.filter((asset) => !editor.getAssetById(asset.id))
if (assetsToCreate.length) {
editor.createAssets(assetsToCreate)
}
// Create the shapes
editor.createShapes(paritals, true)
// Re-position shapes so that the center of the group is at the provided point
const { viewportPageBounds } = editor
let { selectedPageBounds } = editor
if (selectedPageBounds) {
const offset = selectedPageBounds!.center.sub(position)
editor.updateShapes(
paritals.map((partial) => {
return {
id: partial.id,
type: partial.type,
x: partial.x! - offset.x,
y: partial.y! - offset.y,
}
})
)
}
// Zoom out to fit the shapes, if necessary
selectedPageBounds = editor.selectedPageBounds
if (selectedPageBounds && !viewportPageBounds.contains(selectedPageBounds)) {
editor.zoomToSelection()
}
})
}
/**
* Override this method to change how assets are created from files.
*
* @param editor - The editor instance
* @param file - The file to create the asset from.
*/
async createAssetFromFile(_editor: Editor, file: File): Promise<TLAsset> {
return await new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onerror = () => reject(reader.error)
reader.onload = async () => {
let dataUrl = reader.result as string
const isImageType = isImage(file.type)
const sizeFn = isImageType ? getImageSizeFromSrc : getVideoSizeFromSrc
// Hack to make .mov videos work via dataURL.
if (file.type === 'video/quicktime' && dataUrl.includes('video/quicktime')) {
dataUrl = dataUrl.replace('video/quicktime', 'video/mp4')
}
const originalSize = await sizeFn(dataUrl)
const size = containBoxSize(originalSize, { w: MAX_ASSET_WIDTH, h: MAX_ASSET_HEIGHT })
if (size !== originalSize && (file.type === 'image/jpeg' || file.type === 'image/png')) {
// If we created a new size and the type is an image, rescale the image
dataUrl = await getResizedImageDataUrl(dataUrl, size.w, size.h)
}
const assetId: TLAssetId = AssetRecordType.createId(getHashForString(dataUrl))
const metadata = await getFileMetaData(file)
const asset: Extract<TLAsset, { type: 'image' | 'video' }> = {
id: assetId,
type: isImageType ? 'image' : 'video',
typeName: 'asset',
props: {
name: file.name,
src: dataUrl,
w: size.w,
h: size.h,
mimeType: file.type,
isAnimated: metadata.isAnimated,
},
meta: {},
}
resolve(asset)
}
reader.readAsDataURL(file)
})
}
/**
* Override me to change the way assets are created from urls.
*
* @param editor - The editor instance
* @param url - The url to create the asset from
*/
async createAssetFromUrl(_editor: Editor, url: string): Promise<TLAsset> {
let meta: { image: string; title: string; description: string }
try {
const resp = await fetch(url, { method: 'GET', mode: 'no-cors' })
const html = await resp.text()
const doc = new DOMParser().parseFromString(html, 'text/html')
meta = {
image: doc.head.querySelector('meta[property="og:image"]')?.getAttribute('content') ?? '',
title:
doc.head.querySelector('meta[property="og:title"]')?.getAttribute('content') ??
truncateStringWithEllipsis(url, 32),
description:
doc.head.querySelector('meta[property="og:description"]')?.getAttribute('content') ?? '',
}
} catch (error) {
console.error(error)
meta = { image: '', title: truncateStringWithEllipsis(url, 32), description: '' }
}
// Create the bookmark asset from the meta
return {
id: AssetRecordType.createId(getHashForString(url)),
typeName: 'asset',
type: 'bookmark',
props: {
src: url,
description: meta.description,
image: meta.image,
title: meta.title,
},
meta: {},
}
}
}
/* --------------------- Helpers -------------------- */
const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/
/**
* Replace any tabs with double spaces.
* @param text - The text to replace tabs in.
* @internal
*/
function replaceTabsWithSpaces(text: string) {
return text.replace(/\t/g, INDENT)
}
/**
* Strip common minimum indentation from each line.
* @param text - The text to strip.
* @internal
*/
function stripCommonMinimumIndentation(text: string): string {
// Split the text into individual lines
const lines = text.split('\n')
// remove any leading lines that are only whitespace or newlines
while (lines[0].trim().length === 0) {
lines.shift()
}
let minIndentation = Infinity
for (const line of lines) {
if (line.trim().length > 0) {
const indentation = line.length - line.trimStart().length
minIndentation = Math.min(minIndentation, indentation)
}
}
return lines.map((line) => line.slice(minIndentation)).join('\n')
}
/**
* Strip trailing whitespace from each line and remove any trailing newlines.
* @param text - The text to strip.
* @internal
*/
function stripTrailingWhitespace(text: string): string {
return text.replace(/[ \t]+$/gm, '').replace(/\n+$/, '')
}

View file

@ -1,6 +1,6 @@
import { atom, transact } from '@tldraw/state' import { atom, transact } from '@tldraw/state'
import { devFreeze } from '@tldraw/store' import { devFreeze } from '@tldraw/store'
import { uniqueId } from '../../utils/data' import { uniqueId } from '../../utils/uniqueId'
import { TLCommandHandler, TLHistoryEntry } from '../types/history-types' import { TLCommandHandler, TLHistoryEntry } from '../types/history-types'
import { Stack, stack } from './Stack' import { Stack, stack } from './Stack'

View file

@ -1,27 +1,28 @@
import { atom, computed, EMPTY_ARRAY } from '@tldraw/state'
import { TLGroupShape, TLParentId, TLShape, TLShapeId, Vec2dModel } from '@tldraw/tlschema'
import { dedupe, deepCopy } from '@tldraw/utils'
import { import {
Box2d, Box2d,
flipSelectionHandleX, flipSelectionHandleX,
flipSelectionHandleY, flipSelectionHandleY,
isSelectionCorner, isSelectionCorner,
Matrix2d,
rangeIntersection,
rangesOverlap,
SelectionCorner, SelectionCorner,
SelectionEdge, SelectionEdge,
Vec2d, } from '../../primitives/Box2d'
VecLike, import { Matrix2d } from '../../primitives/Matrix2d'
} from '@tldraw/primitives' import { rangeIntersection, rangesOverlap } from '../../primitives/utils'
import { atom, computed, EMPTY_ARRAY } from '@tldraw/state' import { Vec2d, VecLike } from '../../primitives/Vec2d'
import { TLGroupShape, TLParentId, TLShape, TLShapeId, Vec2dModel } from '@tldraw/tlschema' import { uniqueId } from '../../utils/uniqueId'
import { dedupe, deepCopy } from '@tldraw/utils'
import { uniqueId } from '../../utils/data'
import type { Editor } from '../Editor' import type { Editor } from '../Editor'
/** @public */
export type PointsSnapLine = { export type PointsSnapLine = {
id: string id: string
type: 'points' type: 'points'
points: VecLike[] points: VecLike[]
} }
/** @public */
export type GapsSnapLine = { export type GapsSnapLine = {
id: string id: string
type: 'gaps' type: 'gaps'
@ -31,6 +32,8 @@ export type GapsSnapLine = {
endEdge: [VecLike, VecLike] endEdge: [VecLike, VecLike]
}> }>
} }
/** @public */
export type SnapLine = PointsSnapLine | GapsSnapLine export type SnapLine = PointsSnapLine | GapsSnapLine
export type SnapInteractionType = export type SnapInteractionType =
@ -43,6 +46,7 @@ export type SnapInteractionType =
type: 'resize' type: 'resize'
} }
/** @public */
export interface SnapPoint { export interface SnapPoint {
id: string id: string
x: number x: number
@ -208,6 +212,7 @@ function dedupeGapSnaps(snaps: Array<Extract<SnapLine, { type: 'gaps' }>>) {
} }
} }
/** @public */
export class SnapManager { export class SnapManager {
private _snapLines = atom<SnapLine[] | undefined>('snapLines', undefined) private _snapLines = atom<SnapLine[] | undefined>('snapLines', undefined)

View file

@ -1,7 +1,16 @@
import { Box2dModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema' import { Box2dModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
import { uniqueId } from '../../utils/data' import { uniqueId } from '../../utils/uniqueId'
import { Editor } from '../Editor' import { Editor } from '../Editor'
import { TextHelpers } from '../shapes/text/TextHelpers'
const fixNewLines = /\r?\n|\r/g
function normalizeTextForDom(text: string) {
return text
.replace(fixNewLines, '\n')
.split('\n')
.map((x) => x || ' ')
.join('\n')
}
const textAlignmentsForLtr = { const textAlignmentsForLtr = {
start: 'left', start: 'left',
@ -73,7 +82,7 @@ export class TextManager {
elm.style.setProperty('max-width', opts.maxWidth) elm.style.setProperty('max-width', opts.maxWidth)
elm.style.setProperty('padding', opts.padding) elm.style.setProperty('padding', opts.padding)
elm.textContent = TextHelpers.normalizeTextForDom(textToMeasure) elm.textContent = normalizeTextForDom(textToMeasure)
const rect = elm.getBoundingClientRect() const rect = elm.getBoundingClientRect()

View file

@ -1,4 +1,4 @@
import { Vec2d } from '@tldraw/primitives' import { Vec2d } from '../../primitives/Vec2d'
import { Editor } from '../Editor' import { Editor } from '../Editor'
export class TickManager { export class TickManager {

View file

@ -1,5 +1,8 @@
import { Box2d, linesIntersect, pointInPolygon, Vec2d, VecLike } from '@tldraw/primitives'
import { TLBaseShape } from '@tldraw/tlschema' import { TLBaseShape } from '@tldraw/tlschema'
import { Box2d } from '../../primitives/Box2d'
import { Vec2d, VecLike } from '../../primitives/Vec2d'
import { linesIntersect } from '../../primitives/intersect'
import { pointInPolygon } from '../../primitives/utils'
import { ShapeUtil, TLOnResizeHandler } from './ShapeUtil' import { ShapeUtil, TLOnResizeHandler } from './ShapeUtil'
import { resizeBox } from './shared/resizeBox' import { resizeBox } from './shared/resizeBox'

View file

@ -1,17 +1,22 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import { Box2d, linesIntersect, Vec2d, VecLike } from '@tldraw/primitives' import { Migrations } from '@tldraw/store'
import { StyleProp, TLHandle, TLShape, TLShapePartial, TLUnknownShape } from '@tldraw/tlschema' import { ShapeProps, TLHandle, TLShape, TLShapePartial, TLUnknownShape } from '@tldraw/tlschema'
import { Box2d } from '../../primitives/Box2d'
import { Vec2d, VecLike } from '../../primitives/Vec2d'
import { linesIntersect } from '../../primitives/intersect'
import type { Editor } from '../Editor' import type { Editor } from '../Editor'
import { SvgExportContext } from '../types/SvgExportContext'
import { TLResizeHandle } from '../types/selection-types' import { TLResizeHandle } from '../types/selection-types'
import { SvgExportContext } from './shared/SvgExportContext'
/** @public */ /** @public */
export interface TLShapeUtilConstructor< export interface TLShapeUtilConstructor<
T extends TLUnknownShape, T extends TLUnknownShape,
U extends ShapeUtil<T> = ShapeUtil<T> U extends ShapeUtil<T> = ShapeUtil<T>
> { > {
new (editor: Editor, type: T['type'], styleProps: ReadonlyMap<StyleProp<unknown>, string>): U new (editor: Editor): U
type: T['type'] type: T['type']
props?: ShapeProps<T>
migrations?: Migrations
} }
/** @public */ /** @public */
@ -25,27 +30,9 @@ export interface TLShapeUtilCanvasSvgDef {
/** @public */ /** @public */
export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> { export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
constructor( constructor(public editor: Editor) {}
public editor: Editor, static props?: ShapeProps<TLUnknownShape>
public readonly type: Shape['type'], static migrations?: Migrations
public readonly styleProps: ReadonlyMap<StyleProp<unknown>, string>
) {}
setStyleInPartial<T>(
style: StyleProp<T>,
shape: TLShapePartial<Shape>,
value: T
): TLShapePartial<Shape> {
const styleKey = this.styleProps.get(style)
if (!styleKey) return shape
return {
...shape,
props: {
...shape.props,
[styleKey]: value,
},
}
}
/** /**
* The type of the shape util, which should match the shape's type. * The type of the shape util, which should match the shape's type.

View file

@ -1,10 +0,0 @@
import { arrowShapeMigrations, arrowShapeProps } from '@tldraw/tlschema'
import { defineShape } from '../../../config/defineShape'
import { ArrowShapeUtil } from './ArrowShapeUtil'
/** @public */
export const ArrowShape = defineShape('arrow', {
util: ArrowShapeUtil,
props: arrowShapeProps,
migrations: arrowShapeMigrations,
})

Some files were not shown because too many files have changed in this diff Show more