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',
},
},
// {
// 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
- `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
- `store`: an in-memory reactive database
- `tldraw`: the main tldraw package containing both the editor and the UI
- `tlschema`: shape definitions and migrations
- `ui`: the editor's user interface
- `utils`: low-level data utilities shared by other libraries
- `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.waitForSelector('.tl-canvas')
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 { assert } from '@tldraw/utils'
import { rename, writeFile } from 'fs/promises'
import { setupPage } from '../shared-e2e'
let page: Page
declare const editor: Editor
test.describe('Export snapshots', () => {
test.beforeAll(async ({ browser }) => {
page = await browser.newPage()
})
test.beforeEach(async () => {
await setupPage(page)
})
const snapshots = {} as Record<string, TLShapePartial[]>
for (const fill of ['none', 'semi', 'solid', 'pattern']) {
@ -172,7 +163,9 @@ test.describe('Export 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) => {
editor
.updateInstanceState({ exportBackground: false })
@ -188,17 +181,17 @@ test.describe('Export snapshots', () => {
await page.click('[data-testid="menu-item.export-as-svg"]')
const download = await downloadEvent
const path = await download.path()
assert(path)
const path = (await download.path()) as string
// assert(path)
await rename(path, path + '.svg')
await writeFile(
path + '.html',
`
<!DOCTYPE html>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<img src="${path}.svg" />
`,
<!DOCTYPE html>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<img src="${path}.svg" />
`,
'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",
"@playwright/test": "^1.35.1",
"@tldraw/assets": "workspace:*",
"@tldraw/state": "workspace:*",
"@tldraw/tldraw": "workspace:*",
"@tldraw/utils": "workspace:*",
"@tldraw/validate": "workspace:*",
"@vercel/analytics": "^1.0.1",
"lazyrepo": "0.0.0-alpha.27",
"react": "^18.2.0",

View file

@ -2,24 +2,30 @@ import { Tldraw, TLEditorComponents } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
const components: Partial<TLEditorComponents> = {
Brush: ({ brush }) => (
<rect
className="tl-brush"
stroke="red"
fill="none"
width={Math.max(1, brush.w)}
height={Math.max(1, brush.h)}
transform={`translate(${brush.x},${brush.y})`}
/>
),
Brush: function MyBrush({ brush }) {
return (
<svg className="tl-overlays__item">
<rect
className="tl-brush"
stroke="red"
fill="none"
width={Math.max(1, brush.w)}
height={Math.max(1, brush.h)}
transform={`translate(${brush.x},${brush.y})`}
/>
</svg>
)
},
Scribble: ({ scribble, opacity, color }) => {
return (
<polyline
points={scribble.points.map((p) => `${p.x},${p.y}`).join(' ')}
stroke={color ?? 'black'}
opacity={opacity ?? '1'}
fill="none"
/>
<svg className="tl-overlays__item">
<polyline
points={scribble.points.map((p) => `${p.x},${p.y}`).join(' ')}
stroke={color ?? 'black'}
opacity={opacity ?? '1'}
fill="none"
/>
</svg>
)
},
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 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() {
const rRaf = useRef<any>(-1)
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 { throttle } from '@tldraw/utils'
import { useLayoutEffect, useState } from 'react'
const PERSISTENCE_KEY = 'example-3'
export default function PersistenceExample() {
const [store] = useState(() => createTLStore({ shapes: defaultShapes }))
const [store] = useState(() => createTLStore({ shapeUtils: defaultShapeUtils }))
const [loadingState, setLoadingState] = useState<
{ status: 'loading' } | { status: 'ready' } | { status: 'error'; error: string }
>({

View file

@ -4,12 +4,11 @@ import {
DefaultColorStyle,
HTMLContainer,
StyleProp,
T,
TLBaseShape,
TLDefaultColorStyle,
defineShape,
getDefaultColorTheme,
} from '@tldraw/tldraw'
import { T } from '@tldraw/validate'
// 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.
@ -33,6 +32,15 @@ export type CardShape = TLBaseShape<
export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
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 canResize = (_shape: CardShape) => true
override canBind = (_shape: CardShape) => true
@ -87,17 +95,12 @@ export class CardShapeTool extends BaseBoxShapeTool {
static override id = 'card'
static override initial = 'idle'
override shapeType = 'card'
}
export const CardShape = defineShape('card', {
util: CardShapeUtil,
// to use a style prop, you need to describe all the props in your shape.
props: {
props = {
w: T.number,
h: T.number,
// You can re-use tldraw built-in styles...
color: DefaultColorStyle,
// ...or your own custom styles.
filter: MyFilterStyle,
},
})
}
}

View file

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

View file

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

View file

@ -9,7 +9,7 @@ export const uiOverrides: TLUiOverrides = {
kbd: 'c',
readonlyOk: false,
onSelect: () => {
editor.setSelectedTool('card')
editor.setCurrentTool('card')
},
}
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,
} from '@tldraw/tldraw'
import { useState } from 'react'
import { cardShapeMigrations } from './card-shape-migrations'
import { cardShapeProps } from './card-shape-props'
import { ICardShape } from './card-shape-types'
// 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> {
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
override isAspectRatioLocked = (_shape: ICardShape) => false

View file

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

View file

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

View file

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

View file

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

View file

@ -1,14 +1,14 @@
import { createShapeId, Tldraw, TLShapePartial } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
import { ErrorShape } from './ErrorShape'
import { ErrorShape, ErrorShapeUtil } from './ErrorShape'
const shapes = [ErrorShape]
const shapes = [ErrorShapeUtil]
export default function ErrorBoundaryExample() {
return (
<div className="tldraw__editor">
<Tldraw
shapes={shapes}
shapeUtils={shapes}
tools={[]}
components={{
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 class ErrorShapeUtil extends BaseBoxShapeUtil<ErrorShape> {
static override type = 'error' as const
override type = 'error' as const
getDefaultProps() {
return { message: 'Error!', w: 100, h: 100 }
@ -16,5 +15,3 @@ export class ErrorShapeUtil extends BaseBoxShapeUtil<ErrorShape> {
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 {
DefaultErrorFallback,
ErrorBoundary,
setDefaultEditorAssetUrls,
setDefaultUiAssetUrls,
} from '@tldraw/tldraw'
import { DefaultErrorFallback, ErrorBoundary, setDefaultUiAssetUrls } from '@tldraw/tldraw'
import { setDefaultEditorAssetUrls } from '@tldraw/tldraw/src/lib/utils/assetUrls'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { RouterProvider, createBrowserRouter } from 'react-router-dom'
@ -28,6 +24,7 @@ import HideUiExample from './9-hide-ui/HideUiExample'
import ExamplesTldrawLogo from './ExamplesTldrawLogo'
import { ListLink } from './components/ListLink'
import EndToEnd from './end-to-end/end-to-end'
import OnlyEditorExample from './only-editor/OnlyEditor'
import YjsExample from './yjs/YjsExample'
// This example is only used for end to end tests
@ -50,6 +47,11 @@ export const allExamples: Example[] = [
path: '/develop',
element: <ExampleBasic />,
},
{
title: 'Collaboration (with Yjs)',
path: '/yjs',
element: <YjsExample />,
},
{
title: 'Editor API',
path: '/api',
@ -120,11 +122,6 @@ export const allExamples: Example[] = [
path: '/persistence',
element: <PersistenceExample />,
},
{
title: 'Custom styles',
path: '/yjs',
element: <YjsExample />,
},
{
title: 'Custom styles',
path: '/custom-styles',
@ -135,6 +132,11 @@ export const allExamples: Example[] = [
path: '/shape-meta',
element: <ShapeMetaExample />,
},
{
title: 'Only editor',
path: '/only-editor',
element: <OnlyEditorExample />,
},
// not listed
{
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, useEditor } from '@tldraw/tldraw'
import { Tldraw, track, useEditor } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
import { useYjsStore } from './useYjsStore'

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ import fse from 'fs-extra'
import { join } 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 }) {
const editorRoot = join(vscodeDir, 'editor')

View file

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

View file

@ -1,6 +1,4 @@
import { useEditor } from '@tldraw/editor'
import { parseAndLoadDocument } from '@tldraw/file-format'
import { useDefaultHelpers } from '@tldraw/ui'
import { parseAndLoadDocument, useDefaultHelpers, useEditor } from '@tldraw/tldraw'
import React from 'react'
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'
// eslint-disable-next-line import/no-internal-modules
import '@tldraw/editor/editor.css'
import { ContextMenu, TLUiMenuSchema, TldrawUi } from '@tldraw/ui'
// eslint-disable-next-line import/no-internal-modules
import '@tldraw/ui/ui.css'
import '@tldraw/tldraw/tldraw.css'
// eslint-disable-next-line import/no-internal-modules
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 { VscodeMessage } from '../../messages'
import '../public/index.css'
@ -128,26 +125,22 @@ function TldrawInner({ uri, assetSrc, isDarkMode, fileContents }: TLDrawInnerPro
const assetUrls = useMemo(() => getAssetUrlsByImport({ baseUrl: assetSrc }), [assetSrc])
const handleMount = useCallback((editor: Editor) => {
editor.externalContentManager.createAssetFromUrl = onCreateAssetFromUrl
editor.registerExternalAssetHandler('url', onCreateAssetFromUrl)
}, [])
return (
<TldrawEditor
shapes={defaultShapes}
tools={defaultTools}
<Tldraw
shapeUtils={defaultShapeUtils}
tools={defaultShapeTools}
assetUrls={assetUrls}
persistenceKey={uri}
onMount={handleMount}
overrides={[menuOverrides, linksUiOverrides]}
autoFocus
>
{/* <DarkModeHandler themeKind={themeKind} /> */}
<TldrawUi assetUrls={assetUrls} overrides={[menuOverrides, linksUiOverrides]}>
<FileOpen fileContents={fileContents} forceDarkMode={isDarkMode} />
<ChangeResponder />
<ContextMenu>
<Canvas />
</ContextMenu>
</TldrawUi>
</TldrawEditor>
<FileOpen fileContents={fileContents} forceDarkMode={isDarkMode} />
<ChangeResponder />
</Tldraw>
)
}

View file

@ -1,8 +1,13 @@
import { AssetRecordType, Editor, TLAsset, truncateStringWithEllipsis } from '@tldraw/editor'
import { getHashForString } from '@tldraw/utils'
import { AssetRecordType, TLAsset, TLExternalAssetContent, getHashForString } from '@tldraw/tldraw'
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 {
// First, try to get the data from vscode
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'
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 { vscode } from './vscode'
@ -26,7 +26,7 @@ export function rpc(
type ErrorType = VscodeMessagePairs[typeof id]['error']
const type = (id + '/request') as RequestType['type']
const uuid = nanoid()
const uuid = uniqueId()
return new Promise<ResponseType['data']>((resolve, reject) => {
const inMessage = {
uuid,

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import esbuild from 'esbuild'
import { logEnv } from '../../vscode-script-utils/cli'
import { copyEditor, removeDistDirectory } from '../../vscode-script-utils/helpers'
import { logEnv } from './cli'
import { copyEditor, removeDistDirectory } from './helpers'
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 { join } from 'path'
import { logEnv } from '../../vscode-script-utils/cli'
import { copyEditor, removeDistDirectory } from '../../vscode-script-utils/helpers'
import { getDirname } from '../../vscode-script-utils/path'
import { logEnv } from './cli'
import { copyEditor, removeDistDirectory } from './helpers'
import { getDirname } from './path'
const rootDir = getDirname(import.meta.url, '../')
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 { defaultFileContents, fileExists, loadFile } from './file'
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 { TLDrawDocument } from './TldrawDocument'
import { GlobalStateKeys, WebViewMessageHandler } from './WebViewMessageHandler'
// @ts-ignore
export class TldrawWebviewManager {
private disposables: vscode.Disposable[] = []
@ -15,7 +14,7 @@ export class TldrawWebviewManager {
) {
let userId = context.globalState.get(GlobalStateKeys.UserId)
if (!userId) {
userId = 'user:' + nanoid()
userId = 'user:' + uniqueId()
context.globalState.update(GlobalStateKeys.UserId, userId)
}

View file

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

View file

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

View file

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

View file

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

View file

@ -94,5 +94,8 @@
},
"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"
},
"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;
}
.tlui-following {
display: block;
.tl-positioned {
position: absolute;
inset: 0px;
border-width: 2px;
border-style: solid;
z-index: 9999999;
pointer-events: none;
top: 0px;
left: 0px;
transform-origin: top left;
}
/* ------------------- Background ------------------- */

View file

@ -45,24 +45,21 @@
"lint": "yarn run -T tsx ../../scripts/lint.ts"
},
"dependencies": {
"@tldraw/indices": "workspace:*",
"@tldraw/primitives": "workspace:*",
"@tldraw/state": "workspace:*",
"@tldraw/store": "workspace:*",
"@tldraw/tlschema": "workspace:*",
"@tldraw/utils": "workspace:*",
"@tldraw/validate": "workspace:*",
"@types/canvas-size": "^1.2.0",
"@types/core-js": "^2.5.5",
"@use-gesture/react": "^10.2.27",
"canvas-size": "^1.2.6",
"classnames": "^2.3.2",
"escape-string-regexp": "^5.0.0",
"core-js": "^3.31.1",
"eventemitter3": "^4.0.7",
"idb": "^7.1.1",
"is-plain-object": "^5.0.0",
"lodash.throttle": "^4.1.1",
"lodash.uniq": "^4.5.0",
"nanoid": "4.0.2"
"nanoid": "^3.3.6"
},
"peerDependencies": {
"react": "^18",
@ -95,7 +92,7 @@
"^.+\\.*.css$"
],
"transformIgnorePatterns": [
"node_modules/(?!(nanoid|escape-string-regexp)/)"
"node_modules/(?!(nanoid)/)"
],
"moduleNameMapper": {
"^~(.*)": "<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
// incorrect output. https://github.com/evanw/esbuild/issues/1737
// eslint-disable-next-line local/no-export-star
export * from '@tldraw/indices'
export {
EMPTY_ARRAY,
atom,
computed,
react,
track,
transact,
transaction,
useComputed,
useQuickReactor,
useReactor,
useValue,
whyAmIRunning,
type Atom,
type Signal,
} 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
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 {
ErrorScreen,
LoadingScreen,
@ -26,20 +33,16 @@ export {
type TldrawEditorBaseProps,
type TldrawEditorProps,
} from './lib/TldrawEditor'
export {
defaultEditorAssetUrls,
setDefaultEditorAssetUrls,
type TLEditorAssetUrls,
} from './lib/assetUrls'
export { Canvas } from './lib/components/Canvas'
export { DefaultErrorFallback } from './lib/components/DefaultErrorFallback'
export {
ErrorBoundary,
OptionalErrorBoundary,
type TLErrorBoundaryProps,
} from './lib/components/ErrorBoundary'
export { HTMLContainer, type HTMLContainerProps } from './lib/components/HTMLContainer'
export { PositionedOnCanvas } from './lib/components/PositionedOnCanvas'
export { SVGContainer, type SVGContainerProps } from './lib/components/SVGContainer'
export { DefaultErrorFallback } from './lib/components/default-components/DefaultErrorFallback'
export {
TAB_ID,
createSessionStateSnapshotSignal,
@ -60,39 +63,35 @@ export {
type TLStoreOptions,
} from './lib/config/createTLStore'
export { createTLUser } from './lib/config/createTLUser'
export { coreShapes, defaultShapes } from './lib/config/defaultShapes'
export { defaultTools } from './lib/config/defaultTools'
export { defineShape, type TLShapeInfo } from './lib/config/defineShape'
export { coreShapes, type TLAnyShapeUtilConstructor } from './lib/config/defaultShapes'
export {
ANIMATION_MEDIUM_MS,
ANIMATION_SHORT_MS,
CAMERA_SLIDE_FRICTION,
DEFAULT_ANIMATION_OPTIONS,
DOUBLE_CLICK_DURATION,
DRAG_DISTANCE,
GRID_INCREMENT,
GRID_STEPS,
HAND_TOOL_FRICTION,
HASH_PATTERN_ZOOM_NAMES,
MAJOR_NUDGE_FACTOR,
MAX_ASSET_HEIGHT,
MAX_ASSET_WIDTH,
MAX_PAGES,
MAX_SHAPES_PER_PAGE,
MAX_ZOOM,
MINOR_NUDGE_FACTOR,
MIN_ZOOM,
MULTI_CLICK_DURATION,
REMOVE_SYMBOL,
RICH_TYPES,
SVG_PADDING,
ZOOMS,
} from './lib/constants'
export { Editor, type TLAnimationOptions, type TLEditorOptions } from './lib/editor/Editor'
export {
ExternalContentManager as PlopManager,
type TLExternalContent,
} from './lib/editor/managers/ExternalContentManager'
export { ScribbleManager } from './lib/editor/managers/ScribbleManager'
SnapManager,
type GapsSnapLine,
type PointsSnapLine,
type SnapLine,
type SnapPoint,
} from './lib/editor/managers/SnapManager'
export { BaseBoxShapeUtil, type TLBaseBoxShape } from './lib/editor/shapes/BaseBoxShapeUtil'
export {
ShapeUtil,
@ -117,38 +116,25 @@ export {
type TLOnTranslateStartHandler,
type TLResizeInfo,
type TLResizeMode,
type TLShapeUtilCanvasSvgDef,
type TLShapeUtilConstructor,
type TLShapeUtilFlag,
} 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 { HighlightShape } from './lib/editor/shapes/highlight/HighlightShape'
export { HighlightShapeUtil } from './lib/editor/shapes/highlight/HighlightShapeUtil'
export { ImageShape } from './lib/editor/shapes/image/ImageShape'
export { ImageShapeUtil } from './lib/editor/shapes/image/ImageShapeUtil'
export { LineShape } from './lib/editor/shapes/line/LineShape'
export { LineShapeUtil, getSplineForLineShape } from './lib/editor/shapes/line/LineShapeUtil'
export { NoteShape } from './lib/editor/shapes/note/NoteShape'
export { NoteShapeUtil } from './lib/editor/shapes/note/NoteShapeUtil'
export { getArrowheadPathForType } from './lib/editor/shapes/shared/arrow/arrowheads'
export {
getCurvedArrowHandlePath,
getSolidCurvedArrowPath,
} from './lib/editor/shapes/shared/arrow/curved-arrow'
export { getArrowTerminalsInArrowSpace } from './lib/editor/shapes/shared/arrow/shared'
export {
getSolidStraightArrowPath,
getStraightArrowHandlePath,
} from './lib/editor/shapes/shared/arrow/straight-arrow'
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 { 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 TLEventMap, type TLEventMapHandler } from './lib/editor/types/emit-types'
export {
@ -184,6 +170,10 @@ export {
type UiEvent,
type UiEventType,
} from './lib/editor/types/event-types'
export {
type TLExternalAssetContent,
type TLExternalContent,
} from './lib/editor/types/external-content'
export {
type TLCommand,
type TLCommandHandler,
@ -192,83 +182,126 @@ export {
} from './lib/editor/types/history-types'
export { type RequiredKeys } from './lib/editor/types/misc-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 { getCursor } from './lib/hooks/useCursor'
export { useEditor } from './lib/hooks/useEditor'
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 { usePeerIds } from './lib/hooks/usePeerIds'
export { usePresence } from './lib/hooks/usePresence'
export { useSelectionEvents } from './lib/hooks/useSelectionEvents'
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 {
ReadonlySharedStyleMap,
SharedStyleMap,
type SharedStyle,
} from './lib/utils/SharedStylesMap'
export { WeakMapCache } from './lib/utils/WeakMapCache'
export {
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 { dataUrlToFile } from './lib/utils/assets'
export { debugFlags, featureFlags, type DebugFlag } from './lib/utils/debug-flags'
export {
getRotatedBoxShadow,
loopToHtmlElement,
preventDefault,
releasePointerCapture,
setPointerCapture,
truncateStringWithEllipsis,
usePrefersReducedMotion,
stopEventPropagation,
} 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 {
getEmbedInfo,
getEmbedInfoUnsafely,
matchEmbedUrl,
matchUrl,
type TLEmbedResult,
} from './lib/utils/embeds'
getIndexAbove,
getIndexBelow,
getIndexBetween,
getIndices,
getIndicesAbove,
getIndicesBelow,
getIndicesBetween,
sortByIndex,
} from './lib/utils/reordering/reordering'
export {
downloadDataURLAsFile,
getSvgAsDataUrl,
getSvgAsDataUrlSync,
getSvgAsImage,
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'
applyRotationToSnapshotShapes,
getRotationSnapshot,
type TLRotationSnapshot,
} from './lib/utils/rotation'
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 { 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'
/** @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 { TLRecord, TLStore } from '@tldraw/tlschema'
import { RecursivePartial, Required, annotateError } from '@tldraw/utils'
import { Required, annotateError } from '@tldraw/utils'
import React, {
memo,
useCallback,
@ -10,11 +10,11 @@ import React, {
useSyncExternalStore,
} from 'react'
import { TLEditorAssetUrls, useDefaultEditorAssetsWithOverrides } from './assetUrls'
import { DefaultErrorFallback } from './components/DefaultErrorFallback'
import { Canvas } from './components/Canvas'
import { OptionalErrorBoundary } from './components/ErrorBoundary'
import { DefaultErrorFallback } from './components/default-components/DefaultErrorFallback'
import { TLUser, createTLUser } from './config/createTLUser'
import { AnyTLShapeInfo } from './config/defineShape'
import { TLAnyShapeUtilConstructor } from './config/defaultShapes'
import { Editor } from './editor/Editor'
import { TLStateNodeConstructor } from './editor/tools/StateNode'
import { ContainerProvider, useContainer } from './hooks/useContainer'
@ -29,7 +29,6 @@ import {
import { useEvent } from './hooks/useEvent'
import { useForceUpdate } from './hooks/useForceUpdate'
import { useLocalStore } from './hooks/useLocalStore'
import { usePreloadAssets } from './hooks/usePreloadAssets'
import { useSafariFocusOutFix } from './hooks/useSafariFocusOutFix'
import { useZoomCss } from './hooks/useZoomCss'
import { TLStoreWithStatus } from './utils/sync/StoreWithStatus'
@ -65,20 +64,15 @@ export interface TldrawEditorBaseProps {
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.
*/
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.
*/
@ -93,6 +87,11 @@ export interface TldrawEditorBaseProps {
* Called when the editor has mounted.
*/
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
/** @public */
@ -133,7 +132,7 @@ export const TldrawEditor = memo(function TldrawEditor({
// defaults applied in @tldraw/tldraw.
const withDefaults = {
...rest,
shapes: rest.shapes ?? EMPTY_SHAPES_ARRAY,
shapeUtils: rest.shapeUtils ?? EMPTY_SHAPE_UTILS_ARRAY,
tools: rest.tools ?? EMPTY_TOOLS_ARRAY,
}
@ -167,12 +166,12 @@ export const TldrawEditor = memo(function TldrawEditor({
})
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({
shapes,
shapeUtils,
initialData,
persistenceKey,
sessionId,
@ -186,7 +185,10 @@ const TldrawEditorWithLoadingStore = memo(function TldrawEditorBeforeLoading({
store,
user,
...rest
}: Required<TldrawEditorProps & { store: TLStoreWithStatus; user: TLUser }, 'shapes' | 'tools'>) {
}: Required<
TldrawEditorProps & { store: TLStoreWithStatus; user: TLUser },
'shapeUtils' | 'tools'
>) {
const container = useContainer()
useLayoutEffect(() => {
@ -225,16 +227,16 @@ function TldrawEditorWithReadyStore({
children,
store,
tools,
shapes,
shapeUtils,
autoFocus,
user,
assetUrls,
initialState,
}: Required<
TldrawEditorProps & {
store: TLStore
user: TLUser
},
'shapes' | 'tools'
'shapeUtils' | 'tools'
>) {
const { ErrorFallback } = useEditorComponents()
const container = useContainer()
@ -243,10 +245,11 @@ function TldrawEditorWithReadyStore({
useLayoutEffect(() => {
const editor = new Editor({
store,
shapes,
shapeUtils,
tools,
getContainer: () => container,
user,
initialState,
})
;(window as any).app = editor
;(window as any).editor = editor
@ -255,10 +258,10 @@ function TldrawEditorWithReadyStore({
return () => {
editor.dispose()
}
}, [container, shapes, tools, store, user])
}, [container, shapeUtils, tools, store, user, initialState])
React.useLayoutEffect(() => {
if (editor && autoFocus) editor.focus()
if (editor && autoFocus) editor.isFocused = true
}, [editor, autoFocus])
const onMountEvent = useEvent((editor: Editor) => {
@ -288,17 +291,6 @@ function TldrawEditorWithReadyStore({
() => 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) {
return null
}
@ -311,7 +303,7 @@ function TldrawEditorWithReadyStore({
// document in the event of an error to reassure them that their work is
// not lost.
<OptionalErrorBoundary
fallback={ErrorFallback}
fallback={ErrorFallback as any}
onError={(error) =>
editor.annotateError(error, { origin: 'react.tldraw', willCrashApp: true })
}
@ -334,7 +326,7 @@ function Layout({ children }: { children: any }) {
useSafariFocusOutFix()
useForceUpdate()
return children
return children ?? <Canvas />
}
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 { TLHandle, TLShapeId } from '@tldraw/tlschema'
import { dedupe, modulate, objectMapValues } from '@tldraw/utils'
@ -12,10 +11,10 @@ import { useFixSafariDoubleTapZoomPencilEvents } from '../hooks/useFixSafariDoub
import { useGestureEvents } from '../hooks/useGestureEvents'
import { useHandleEvents } from '../hooks/useHandleEvents'
import { useScreenBounds } from '../hooks/useScreenBounds'
import { Matrix2d } from '../primitives/Matrix2d'
import { toDomPrecision } from '../primitives/utils'
import { debugFlags } from '../utils/debug-flags'
import { LiveCollaborators } from './LiveCollaborators'
import { SelectionBg } from './SelectionBg'
import { SelectionFg } from './SelectionFg'
import { Shape } from './Shape'
import { ShapeIndicator } from './ShapeIndicator'
@ -97,7 +96,7 @@ export const Canvas = track(function Canvas() {
{SvgDefs && <SvgDefs />}
</defs>
</svg>
<SelectionBg />
<SelectionBackgroundWrapper />
<div className="tl-shapes">
<ShapesToDisplay />
</div>
@ -110,7 +109,7 @@ export const Canvas = track(function Canvas() {
<HoveredShapeIndicator />
<HintedShapeIndicator />
<SnapLinesWrapper />
<SelectionFg />
<SelectionForegroundWrapper />
<LiveCollaborators />
</div>
</div>
@ -124,12 +123,13 @@ const GridWrapper = track(function GridWrapper() {
// get grid from context
const { gridSize } = editor.documentSettings
const { x, y, z } = editor.camera
const isGridMode = editor.isGridMode
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() {
@ -432,3 +432,15 @@ const UiLogger = track(() => {
</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 { TLErrorFallbackComponent } from './DefaultErrorFallback'
import { TLErrorFallbackComponent } from './default-components/DefaultErrorFallback'
/** @public */
export interface TLErrorBoundaryProps {
children: React.ReactNode
onError?: ((error: unknown) => void) | null
fallback: (props: { error: unknown }) => any
fallback: TLErrorFallbackComponent
}
type TLErrorBoundaryState = { error: Error | null }
@ -21,13 +21,13 @@ export class ErrorBoundary extends React.Component<
return { error }
}
state = initialState
override state = initialState
componentDidCatch(error: unknown) {
override componentDidCatch(error: unknown) {
this.props.onError?.(error)
}
render() {
override render() {
const { error } = this.state
if (error !== null) {
@ -52,7 +52,7 @@ export function OptionalErrorBoundary({
}
return (
<ErrorBoundary fallback={fallback} {...props}>
<ErrorBoundary fallback={fallback as any} {...props}>
{children}
</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 { TLShape, TLShapeId } from '@tldraw/tlschema'
import * as React from 'react'
import { useEditor } from '../..'
import { ShapeUtil } from '../editor/shapes/ShapeUtil'
import { useEditor } from '../hooks/useEditor'
import { useEditorComponents } from '../hooks/useEditorComponents'
import { useShapeEvents } from '../hooks/useShapeEvents'
import { Matrix2d } from '../primitives/Matrix2d'
import { OptionalErrorBoundary } from './ErrorBoundary'
/*
@ -135,7 +135,7 @@ export const Shape = track(function Shape({
{isCulled && util.canUnmount(shape) ? (
<CulledShape shape={shape} />
) : (
<OptionalErrorBoundary fallback={ShapeErrorFallback} onError={annotateError}>
<OptionalErrorBoundary fallback={ShapeErrorFallback as any} onError={annotateError}>
<InnerShape shape={shape} util={util} />
</OptionalErrorBoundary>
)}
@ -146,7 +146,7 @@ export const Shape = track(function Shape({
const InnerShape = React.memo(
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
)
@ -159,7 +159,7 @@ const InnerShapeBackground = React.memo(
shape: 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
)

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,9 @@
import { ComponentType } from 'react'
/** @public */
export type TLShapeIndicatorErrorFallback = (props: { error: unknown }) => any | null
export type TLShapeIndicatorErrorFallbackComponent = ComponentType<{ error: unknown }>
/** @internal */
export const DefaultShapeIndicatorErrorFallback: TLShapeIndicatorErrorFallback = () => {
export const DefaultShapeIndicatorErrorFallback: TLShapeIndicatorErrorFallbackComponent = () => {
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 * as React from 'react'
import {
type GapsSnapLine,
type PointsSnapLine,
type SnapLine,
} from '../editor/managers/SnapManager'
} from '../../editor/managers/SnapManager'
import { rangeIntersection } from '../../primitives/utils'
function PointsSnapLine({ points, zoom }: { zoom: number } & PointsSnapLine) {
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
line: SnapLine
zoom: number
}) => any
}>
export const DefaultSnapLine: TLSnapLineComponent = ({ className, line, zoom }) => {
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 = () => {
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'
import { objectMapFromEntries } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { uniqueId } from '../utils/data'
import { uniqueId } from '../utils/uniqueId'
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 { getDefaultTranslationLocale } from '@tldraw/tlschema'
import { T } from '@tldraw/validate'
import { uniqueId } from '../utils/data'
import { uniqueId } from '../utils/uniqueId'
const USER_DATA_KEY = 'TLDRAW_USER_DATA_v3'

View file

@ -1,13 +1,23 @@
import { HistoryEntry, SerializedStore, Store, StoreSchema } from '@tldraw/store'
import { TLRecord, TLStore, TLStoreProps, createTLSchema } from '@tldraw/tlschema'
import { checkShapesAndAddCore } from './defaultShapes'
import { AnyTLShapeInfo, TLShapeInfo } from './defineShape'
import {
SchemaShapeInfo,
TLRecord,
TLStore,
TLStoreProps,
TLUnknownShape,
createTLSchema,
} from '@tldraw/tlschema'
import { TLShapeUtilConstructor } from '../editor/shapes/ShapeUtil'
import { TLAnyShapeUtilConstructor, checkShapesAndAddCore } from './defaultShapes'
/** @public */
export type TLStoreOptions = {
initialData?: SerializedStore<TLRecord>
defaultName?: string
} & ({ shapes: readonly AnyTLShapeInfo[] } | { schema: StoreSchema<TLRecord, TLStoreProps> })
} & (
| { shapeUtils: readonly TLAnyShapeUtilConstructor[] }
| { schema: StoreSchema<TLRecord, TLStoreProps> }
)
/** @public */
export type TLStoreEventInfo = HistoryEntry<TLRecord>
@ -22,7 +32,7 @@ export function createTLStore({ initialData, defaultName = '', ...rest }: TLStor
const schema =
'schema' in rest
? rest.schema
: createTLSchema({ shapes: shapesArrayToShapeMap(checkShapesAndAddCore(rest.shapes)) })
: createTLSchema({ shapes: shapesArrayToShapeMap(checkShapesAndAddCore(rest.shapeUtils)) })
return new Store({
schema,
initialData,
@ -32,6 +42,14 @@ export function createTLStore({ initialData, defaultName = '', ...rest }: TLStor
})
}
function shapesArrayToShapeMap(shapes: TLShapeInfo[]) {
return Object.fromEntries(shapes.map((s) => [s.type, s]))
function shapesArrayToShapeMap(shapeUtils: TLShapeUtilConstructor<TLUnknownShape>[]) {
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 { BookmarkShape } from '../editor/shapes/bookmark/BookmarkShape'
import { DrawShape } from '../editor/shapes/draw/DrawShape'
import { EmbedShape } from '../editor/shapes/embed/EmbedShape'
import { FrameShape } from '../editor/shapes/frame/FrameShape'
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'
import { TLShapeUtilConstructor } from '../editor/shapes/ShapeUtil'
import { GroupShapeUtil } from '../editor/shapes/group/GroupShapeUtil'
/** @public */
export type TLAnyShapeUtilConstructor = TLShapeUtilConstructor<any>
/** @public */
export const coreShapes = [
// created by grouping interactions, probably the corest core shape that we have
GroupShape,
// 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,
GroupShapeUtil,
] as const
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>()
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 */
export const MAX_SHAPES_PER_PAGE = 2000
/** @internal */
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 */
export const ANIMATION_SHORT_MS = 80
/** @internal */
@ -44,17 +33,9 @@ export const MAJOR_NUDGE_FACTOR = 10
/** @internal */
export const MINOR_NUDGE_FACTOR = 1
/** @internal */
export const MAX_ASSET_WIDTH = 1000
/** @internal */
export const MAX_ASSET_HEIGHT = 1000
/** @internal */
export const GRID_INCREMENT = 5
/** @internal */
export const MIN_CROP_SIZE = 8
/** @internal */
export const DOUBLE_CLICK_DURATION = 450
/** @internal */
@ -84,7 +65,7 @@ export const DEFAULT_ANIMATION_OPTIONS = {
}
/** @internal */
export const HAND_TOOL_FRICTION = 0.09
export const CAMERA_SLIDE_FRICTION = 0.09
/** @public */
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 {
COARSE_DRAG_DISTANCE,
DOUBLE_CLICK_DURATION,
DRAG_DISTANCE,
MULTI_CLICK_DURATION,
} from '../../constants'
import { uniqueId } from '../../utils/data'
import { Vec2d } from '../../primitives/Vec2d'
import { uniqueId } from '../../utils/uniqueId'
import type { Editor } from '../Editor'
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 { devFreeze } from '@tldraw/store'
import { uniqueId } from '../../utils/data'
import { uniqueId } from '../../utils/uniqueId'
import { TLCommandHandler, TLHistoryEntry } from '../types/history-types'
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 {
Box2d,
flipSelectionHandleX,
flipSelectionHandleY,
isSelectionCorner,
Matrix2d,
rangeIntersection,
rangesOverlap,
SelectionCorner,
SelectionEdge,
Vec2d,
VecLike,
} from '@tldraw/primitives'
import { atom, computed, EMPTY_ARRAY } from '@tldraw/state'
import { TLGroupShape, TLParentId, TLShape, TLShapeId, Vec2dModel } from '@tldraw/tlschema'
import { dedupe, deepCopy } from '@tldraw/utils'
import { uniqueId } from '../../utils/data'
} from '../../primitives/Box2d'
import { Matrix2d } from '../../primitives/Matrix2d'
import { rangeIntersection, rangesOverlap } from '../../primitives/utils'
import { Vec2d, VecLike } from '../../primitives/Vec2d'
import { uniqueId } from '../../utils/uniqueId'
import type { Editor } from '../Editor'
/** @public */
export type PointsSnapLine = {
id: string
type: 'points'
points: VecLike[]
}
/** @public */
export type GapsSnapLine = {
id: string
type: 'gaps'
@ -31,6 +32,8 @@ export type GapsSnapLine = {
endEdge: [VecLike, VecLike]
}>
}
/** @public */
export type SnapLine = PointsSnapLine | GapsSnapLine
export type SnapInteractionType =
@ -43,6 +46,7 @@ export type SnapInteractionType =
type: 'resize'
}
/** @public */
export interface SnapPoint {
id: string
x: number
@ -208,6 +212,7 @@ function dedupeGapSnaps(snaps: Array<Extract<SnapLine, { type: 'gaps' }>>) {
}
}
/** @public */
export class SnapManager {
private _snapLines = atom<SnapLine[] | undefined>('snapLines', undefined)

View file

@ -1,7 +1,16 @@
import { Box2dModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
import { uniqueId } from '../../utils/data'
import { uniqueId } from '../../utils/uniqueId'
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 = {
start: 'left',
@ -73,7 +82,7 @@ export class TextManager {
elm.style.setProperty('max-width', opts.maxWidth)
elm.style.setProperty('padding', opts.padding)
elm.textContent = TextHelpers.normalizeTextForDom(textToMeasure)
elm.textContent = normalizeTextForDom(textToMeasure)
const rect = elm.getBoundingClientRect()

View file

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

View file

@ -1,5 +1,8 @@
import { Box2d, linesIntersect, pointInPolygon, Vec2d, VecLike } from '@tldraw/primitives'
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 { resizeBox } from './shared/resizeBox'

View file

@ -1,17 +1,22 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Box2d, linesIntersect, Vec2d, VecLike } from '@tldraw/primitives'
import { StyleProp, TLHandle, TLShape, TLShapePartial, TLUnknownShape } from '@tldraw/tlschema'
import { Migrations } from '@tldraw/store'
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 { SvgExportContext } from '../types/SvgExportContext'
import { TLResizeHandle } from '../types/selection-types'
import { SvgExportContext } from './shared/SvgExportContext'
/** @public */
export interface TLShapeUtilConstructor<
T extends TLUnknownShape,
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']
props?: ShapeProps<T>
migrations?: Migrations
}
/** @public */
@ -25,27 +30,9 @@ export interface TLShapeUtilCanvasSvgDef {
/** @public */
export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
constructor(
public editor: Editor,
public readonly type: Shape['type'],
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,
},
}
}
constructor(public editor: Editor) {}
static props?: ShapeProps<TLUnknownShape>
static migrations?: Migrations
/**
* 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