ShapeUtil refactor, Editor cleanup (#1611)

This PR improves the ergonomics of `ShapeUtil` classes.

### Cached methods

First, I've remove the cached methods (such as `bounds`) from the
`ShapeUtil` class and lifted this to the `Editor` class.

Previously, calling `ShapeUtil.getBounds` would return the un-cached
bounds of a shape, while calling `ShapeUtil.bounds` would return the
cached bounds of a shape. We also had `Editor.getBounds`, which would
call `ShapeUtil.bounds`. It was confusing. The cached methods like
`outline` were also marked with "please don't override", which suggested
the architecture was just wrong.

The only weirdness from this is that utils sometimes reach out to the
editor for cached versions of data rather than calling their own cached
methods. It's still an easier story to tell than what we had before.

### More defaults

We now have three and only three `abstract` methods for a `ShapeUtil`:
- `getDefaultProps` (renamed from `defaultProps`)
- `getBounds`,
- `component`
-  `indicator`

Previously, we also had `getCenter` as an abstract method, though this
was usually just the middle of the bounds anyway.

### Editing bounds

This PR removes the concept of editingBounds. The viewport will no
longer animate to editing shapes.

### Active area manager

This PR also removes the active area manager, which was not being used
in the way we expected it to be.

### Dpr manager

This PR removes the dpr manager and uses a hook instead to update it
from React. This is one less runtime browser dependency in the app, one
less thing to document.

### Moving things around

This PR also continues to try to organize related methods and properties
in the editor.

### Change Type

- [x] `major` — Breaking change

### Release Notes

- [editor] renames `defaultProps` to `getDefaultProps`
- [editor] removes `outline`, `outlineSegments`, `handles`, `bounds`
- [editor] renames `renderBackground` to `backgroundComponent`
This commit is contained in:
Steve Ruiz 2023-06-19 15:01:18 +01:00 committed by GitHub
parent 38d74a9ff0
commit 57bb341593
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
63 changed files with 6422 additions and 6370 deletions

View file

@ -11,6 +11,126 @@ keywords:
- utils - utils
--- ---
Coming soon. In tldraw, **shapes** are the things that are on the canvas. This article is about shapes: what they are, how they work, and how to create your own shapes. If you'd prefer to see an example, see the tldraw repository's [examples app](https://github.com/tldraw/tldraw/tree/main/apps/examples) for examples of how to create custom shapes in tldraw.
See the [tldraw repository](https://github.com/tldraw/tldraw/tree/main/apps/examples) for an example of how to create custom shapes in tldraw. ## Custom shapes
Let's create a custom "card" shape.
### Shape type
In tldraw's data model, each shape is represented by a JSON object. Let's first create a type that describes what this object will look like.
```ts
import { TLBaseShape } from '@tldraw/tldraw'
type CardShape = TLBaseShape<
'card',
{ w: number, h: number }
>
```
With the `TLBaseShape` helper, we define the shape's `type` property (`card`) and the shape's `props` property (`{ w: number, h: number }`). The type can be any string but the props must be a regular [JSON-serializable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description) JavaScript object.
The `TLBaseShape` helper adds the other default properties of a shape, such as `parentId`, `x`, `y`, and `rotation`.
### Shape Util
While tldraw's shapes themselves are simple JSON objects, we use `ShapeUtil` classes to answer questions about shapes. For example, when the editor needs to know the bounding box of our card shape, it will find a `ShapeUtil` for the `card` type and call that util's `bounds` method, passing in the `CardShape` object as an argument.
Let's create a `ShapeUtil` class for the shape.
```tsx
import { ShapeUtil, HTMLContainer } from '@tldraw/tldraw'
class CardShapeUtil extends ShapeUtil<CardShape> {
static type = 'card' as const
getDefaultProps(): CardShape['props'] {
return {
w: 100,
h: 100,
}
}
getBounds(shape: Shape) {
return new Box2d(0, 0, shape.props.w, shape.props.h)
}
component(shape: Shape) {
return (
<HTMLContainer>Hello</HTMLContainer>
)
}
indicator(shape: Shape) {
return (
<rect width={shape.props.w} height={shape.props.h}/>
)
}
}
```
This is a minimal `ShapeUtil`. We've given it a static property `type` that matches the type of our shape, we've provided implementations for the abstract methods `getDefaultProps`, `getBounds`, `component`, and `indicator`.
We still have work to do on the `CardShapeUtil` class, but we'll come back to it later. For now, let's put the shape onto the canvas by passing it to the `<Tldraw>` component.
### Defining the shape
Before we pass the shape down, we need to package it up in a way using the `defineShape` function. We can then create an array of our defined shapes and pass them into the `<Tldraw>` component's `shapes` prop.
```tsx
import { Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
const MyCardShape = defineShape('card', { util: CardShapeUtil })
const MyCustomShapes = [MyCardShape]
export default function () {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw shapes={MyCustomShapes}/>
</div>
)
}
```
The `defineShape` function can also be used to include a tool that we can use to create this type of shape. For now, let's create it using the `Editor` API.
```tsx
export default function () {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw shapes={MyCustomShapes} onMount={editor => {
editor.createShapes([{ type: "card" }])
}}/>
</div>
)
}
```
Once the page refreshes, we should now have our custom shape on the canvas.
## Using starter shapes
You can use "starter" shape utils like `BaseBoxShapeUtil` to get regular rectangular shape behavior.
> todo
## Flags
You can use flags like `hideRotateHandle` to hide different parts of the UI when the shape is selected, or else to control different behaviors of the shape.
> todo
## Interaction
You can turn on `pointer-events` to allow users to interact inside of the shape.
> todo
## Editing
You can make shapes "editable" to help decide when they're interactive or not.
> todo

View file

@ -10,8 +10,8 @@ import {
} from '@tldraw/tldraw' } from '@tldraw/tldraw'
import { T } from '@tldraw/validate' import { T } from '@tldraw/validate'
// Define a style that can be used across multiple shapes. The ID (myApp:filter) must be globally // Define a style that can be used across multiple shapes.
// unique, so we recommend prefixing it with a namespace. // The ID (myApp:filter) must be globally unique, so we recommend prefixing it with a namespace.
export const MyFilterStyle = StyleProp.defineEnum('myApp:filter', { export const MyFilterStyle = StyleProp.defineEnum('myApp:filter', {
defaultValue: 'none', defaultValue: 'none',
values: ['none', 'invert', 'grayscale', 'blur'], values: ['none', 'invert', 'grayscale', 'blur'],
@ -30,16 +30,13 @@ export type CardShape = TLBaseShape<
> >
export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> { export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
// Id — the shape util's id
static override type = 'card' as const static override type = 'card' as const
// Flags — there are a LOT of other flags!
override isAspectRatioLocked = (_shape: CardShape) => false override isAspectRatioLocked = (_shape: CardShape) => false
override canResize = (_shape: CardShape) => true override canResize = (_shape: CardShape) => true
override canBind = (_shape: CardShape) => true override canBind = (_shape: CardShape) => true
// Default props — used for shapes created with the tool override getDefaultProps(): CardShape['props'] {
override defaultProps(): CardShape['props'] {
return { return {
w: 300, w: 300,
h: 300, h: 300,
@ -48,9 +45,8 @@ export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
} }
} }
// The React component that will be rendered for the shape; can return any HTML elements here
component(shape: CardShape) { component(shape: CardShape) {
const bounds = this.bounds(shape) const bounds = this.editor.getBounds(shape)
return ( return (
<HTMLContainer <HTMLContainer
@ -71,7 +67,7 @@ export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
) )
} }
// The indicator shown when hovering over a shape or when it's selected; must return only SVG elements here // Indicator — used when hovering over a shape or when it's selected; must return only SVG elements here
indicator(shape: CardShape) { indicator(shape: CardShape) {
return <rect width={shape.props.w} height={shape.props.h} /> return <rect width={shape.props.w} height={shape.props.h} />
} }

View file

@ -1,8 +1,8 @@
import { TLUiMenuGroup, Tldraw, menuItem, toolbarItem, useEditor } from '@tldraw/tldraw' import { Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css' import '@tldraw/tldraw/tldraw.css'
import { TLUiOverrides } from '@tldraw/ui/src/lib/overrides' import { CardShape } from './CardShape'
import { track } from 'signia-react' import { FilterStyleUi } from './FilterStyleUi'
import { CardShape, MyFilterStyle } from './CardShape' import { uiOverrides } from './ui-overrides'
const shapes = [CardShape] const shapes = [CardShape]
@ -13,72 +13,10 @@ export default function CustomStylesExample() {
autoFocus autoFocus
persistenceKey="custom-styles-example" persistenceKey="custom-styles-example"
shapes={shapes} shapes={shapes}
overrides={cardToolMenuItems} overrides={uiOverrides}
> >
<FilterStyleUi /> <FilterStyleUi />
</Tldraw> </Tldraw>
</div> </div>
) )
} }
const FilterStyleUi = track(function FilterStyleUi() {
const editor = useEditor()
const filterStyle = editor.sharedStyles.get(MyFilterStyle)
// if the filter style isn't in sharedStyles, it means it's not relevant to the current tool/selection
if (!filterStyle) return null
return (
<div style={{ position: 'absolute', zIndex: 300, top: 64, left: 12 }}>
filter:{' '}
<select
value={filterStyle.type === 'mixed' ? 'mixed' : filterStyle.value}
onChange={(e) => editor.setStyle(MyFilterStyle, e.target.value)}
>
<option value="mixed" disabled>
Mixed
</option>
<option value="none">None</option>
<option value="invert">Invert</option>
<option value="grayscale">Grayscale</option>
<option value="blur">Blur</option>
</select>
</div>
)
})
const cardToolMenuItems: TLUiOverrides = {
// In order for our custom tool to show up in the UI...
// We need to add it to the tools list. This "toolItem"
// has information about its icon, label, keyboard shortcut,
// and what to do when it's selected.
tools(editor, tools) {
tools.card = {
id: 'card',
icon: 'color',
label: 'Card' as any,
kbd: 'c',
readonlyOk: false,
onSelect: () => {
editor.setSelectedTool('card')
},
}
return tools
},
toolbar(_app, toolbar, { tools }) {
// The toolbar is an array of items. We can add it to the
// end of the array or splice it in, then return the array.
toolbar.splice(4, 0, toolbarItem(tools.card))
return toolbar
},
keyboardShortcutsMenu(_app, keyboardShortcutsMenu, { tools }) {
// Same for the keyboard shortcuts menu, but this menu contains
// both items and groups. We want to find the "Tools" group and
// add it to that before returning the array.
const toolsGroup = keyboardShortcutsMenu.find(
(group) => group.id === 'shortcuts-dialog.tools'
) as TLUiMenuGroup
toolsGroup.children.push(menuItem(tools.card))
return keyboardShortcutsMenu
},
}

View file

@ -0,0 +1,29 @@
import { useEditor } from '@tldraw/tldraw'
import { track } from 'signia-react'
import { MyFilterStyle } from './CardShape'
export const FilterStyleUi = track(function FilterStyleUi() {
const editor = useEditor()
const filterStyle = editor.sharedStyles.get(MyFilterStyle)
// if the filter style isn't in sharedStyles, it means it's not relevant to the current tool/selection
if (!filterStyle) return null
return (
<div style={{ position: 'absolute', zIndex: 300, top: 64, left: 12 }}>
filter:{' '}
<select
value={filterStyle.type === 'mixed' ? 'mixed' : filterStyle.value}
onChange={(e) => editor.setStyle(MyFilterStyle, e.target.value)}
>
<option value="mixed" disabled>
Mixed
</option>
<option value="none">None</option>
<option value="invert">Invert</option>
<option value="grayscale">Grayscale</option>
<option value="blur">Blur</option>
</select>
</div>
)
})

View file

@ -0,0 +1,28 @@
import { TLUiMenuGroup, TLUiOverrides, menuItem, toolbarItem } from '@tldraw/tldraw'
export const uiOverrides: TLUiOverrides = {
tools(editor, tools) {
tools.card = {
id: 'card',
icon: 'color',
label: 'Card' as any,
kbd: 'c',
readonlyOk: false,
onSelect: () => {
editor.setSelectedTool('card')
},
}
return tools
},
toolbar(_app, toolbar, { tools }) {
toolbar.splice(4, 0, toolbarItem(tools.card))
return toolbar
},
keyboardShortcutsMenu(_app, keyboardShortcutsMenu, { tools }) {
const toolsGroup = keyboardShortcutsMenu.find(
(group) => group.id === 'shortcuts-dialog.tools'
) as TLUiMenuGroup
toolsGroup.children.push(menuItem(tools.card))
return keyboardShortcutsMenu
},
}

View file

@ -1,71 +0,0 @@
import {
BaseBoxShapeTool,
BaseBoxShapeUtil,
HTMLContainer,
TLBaseShape,
defineShape,
} from '@tldraw/tldraw'
export type CardShape = TLBaseShape<
'card',
{
w: number
h: number
}
>
export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
// Id — the shape util's id
static override type = 'card' as const
// Flags — there are a LOT of other flags!
override isAspectRatioLocked = (_shape: CardShape) => false
override canResize = (_shape: CardShape) => true
override canBind = (_shape: CardShape) => true
// Default props — used for shapes created with the tool
override defaultProps(): CardShape['props'] {
return {
w: 300,
h: 300,
}
}
// The React component that will be rendered for the shape; can return any HTML elements here
component(shape: CardShape) {
const bounds = this.bounds(shape)
return (
<HTMLContainer
id={shape.id}
style={{
border: '1px solid black',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'all',
}}
>
{bounds.w.toFixed()}x{bounds.h.toFixed()}
</HTMLContainer>
)
}
// Indicator — used when hovering over a shape or when it's selected; must return only SVG elements here
indicator(shape: CardShape) {
return <rect width={shape.props.w} height={shape.props.h} />
}
}
// Extending the base box shape tool gives us a lot of functionality for free.
export class CardShapeTool extends BaseBoxShapeTool {
static override id = 'card'
static override initial = 'idle'
override shapeType = CardShapeUtil
}
export const CardShape = defineShape('card', {
util: CardShapeUtil,
tool: CardShapeTool,
})

View file

@ -0,0 +1,17 @@
import { defineShape } from '@tldraw/tldraw'
import { CardShapeTool } from './CardShapeTool'
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)
tool: CardShapeTool,
// A validation schema for the shape's props (optional)
props: cardShapeProps,
// Migrations for upgrading shapes (optional)
migrations: cardShapeMigrations,
})

View file

@ -0,0 +1,16 @@
import { BaseBoxShapeTool, TLClickEvent } from '@tldraw/tldraw'
import { CardShapeUtil } from './CardShapeUtil'
// A tool used to create our custom card shapes. Extending the base
// box shape tool gives us a lot of functionality for free.
export class CardShapeTool extends BaseBoxShapeTool {
static override id = 'card'
static override initial = 'idle'
override shapeType = CardShapeUtil
override onDoubleClick: TLClickEvent = (_info) => {
// you can handle events in handlers like this one;
// check the BaseBoxShapeTool source as an example
}
}

View file

@ -0,0 +1,61 @@
import { resizeBox } from '@tldraw/editor/src/lib/editor/shapes/shared/resizeBox'
import { Box2d, HTMLContainer, ShapeUtil, TLOnResizeHandler } from '@tldraw/tldraw'
import { ICardShape } from './card-shape-types'
// A utility class for the card shape. This is where you define
// the shape's behavior, how it renders (its component and
// indicator), and how it handles different events.
export class CardShapeUtil extends ShapeUtil<ICardShape> {
static override type = 'card' as const
// Flags
override isAspectRatioLocked = (_shape: ICardShape) => false
override canResize = (_shape: ICardShape) => true
override canBind = (_shape: ICardShape) => true
getDefaultProps(): ICardShape['props'] {
return {
w: 300,
h: 300,
color: 'black',
weight: 'regular',
}
}
getBounds(shape: ICardShape) {
return new Box2d(0, 0, shape.props.w, shape.props.h)
}
// Render method — the React component that will be rendered for the shape
component(shape: ICardShape) {
const bounds = this.editor.getBounds(shape)
return (
<HTMLContainer
id={shape.id}
style={{
border: '1px solid black',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'all',
fontWeight: shape.props.weight,
color: `var(--palette-${shape.props.color})`,
}}
>
{bounds.w.toFixed()}x{bounds.h.toFixed()}
</HTMLContainer>
)
}
// Indicator — used when hovering over a shape or when it's selected; must return only SVG elements here
indicator(shape: ICardShape) {
return <rect width={shape.props.w} height={shape.props.h} />
}
// Events
override onResize: TLOnResizeHandler<ICardShape> = (shape, info) => {
return resizeBox(shape, info)
}
}

View file

@ -0,0 +1,21 @@
import { defineMigrations } from '@tldraw/tldraw'
// Migrations for the custom card shape (optional but very helpful)
export const cardShapeMigrations = defineMigrations({
currentVersion: 1,
migrators: {
1: {
// for example, removing a property from the shape
up(shape) {
const migratedUpShape = { ...shape }
delete migratedUpShape._somePropertyToRemove
return migratedUpShape
},
down(shape) {
const migratedDownShape = { ...shape }
migratedDownShape._somePropertyToRemove = 'some value'
return migratedDownShape
},
},
},
})

View file

@ -0,0 +1,17 @@
import { DefaultColorStyle, ShapeProps, StyleProp } from '@tldraw/tldraw'
import { T } from '@tldraw/validate'
import { ICardShape, IWeightStyle } from './card-shape-types'
export const WeightStyle = new StyleProp<IWeightStyle>(
'myApp:weight',
'regular',
T.literalEnum('regular', 'bold')
)
// Validation for our custom card shape's props, using our custom style + one of tldraw's default styles
export const cardShapeProps: ShapeProps<ICardShape> = {
w: T.number,
h: T.number,
color: DefaultColorStyle,
weight: WeightStyle,
}

View file

@ -0,0 +1,15 @@
import { TLBaseShape, TLDefaultColorStyle } from '@tldraw/tldraw'
// We'll have a custom style called weight
export type IWeightStyle = 'regular' | 'bold'
// A type for our custom card shape
export type ICardShape = TLBaseShape<
'card',
{
w: number
h: number
color: TLDefaultColorStyle
weight: IWeightStyle
}
>

View file

@ -1,50 +1,17 @@
import { TLUiMenuGroup, Tldraw, menuItem, toolbarItem } from '@tldraw/tldraw' import { Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css' import '@tldraw/tldraw/tldraw.css'
import { CardShape } from './CardShape' import { customShapes } from './custom-shapes'
import { uiOverrides } from './ui-overrides'
const shapes = [CardShape]
export default function CustomConfigExample() { export default function CustomConfigExample() {
return ( return (
<div className="tldraw__editor"> <div className="tldraw__editor">
<Tldraw <Tldraw
autoFocus autoFocus
shapes={shapes} // Pass in the array of custom shape definitions
overrides={{ shapes={customShapes}
// In order for our custom tool to show up in the UI... // Pass in any overrides to the user interface
// We need to add it to the tools list. This "toolItem" overrides={uiOverrides}
// has information about its icon, label, keyboard shortcut,
// and what to do when it's selected.
tools(editor, tools) {
tools.card = {
id: 'card',
icon: 'color',
label: 'Card' as any,
kbd: 'c',
readonlyOk: false,
onSelect: () => {
editor.setSelectedTool('card')
},
}
return tools
},
toolbar(_app, toolbar, { tools }) {
// The toolbar is an array of items. We can add it to the
// end of the array or splice it in, then return the array.
toolbar.splice(4, 0, toolbarItem(tools.card))
return toolbar
},
keyboardShortcutsMenu(_app, keyboardShortcutsMenu, { tools }) {
// Same for the keyboard shortcuts menu, but this menu contains
// both items and groups. We want to find the "Tools" group and
// add it to that before returning the array.
const toolsGroup = keyboardShortcutsMenu.find(
(group) => group.id === 'shortcuts-dialog.tools'
) as TLUiMenuGroup
toolsGroup.children.push(menuItem(tools.card))
return keyboardShortcutsMenu
},
}}
/> />
</div> </div>
) )

View file

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

View file

@ -0,0 +1,33 @@
import { TLUiMenuGroup, TLUiOverrides, menuItem, toolbarItem } from '@tldraw/tldraw'
// In order to see select our custom shape tool, we need to add it to the ui.
export const uiOverrides: TLUiOverrides = {
tools(editor, tools) {
// Create a tool item in the ui's context.
tools.card = {
id: 'card',
icon: 'color',
label: 'Card' as any,
kbd: 'c',
readonlyOk: false,
onSelect: () => {
editor.setSelectedTool('card')
},
}
return tools
},
toolbar(_app, toolbar, { tools }) {
// Add the tool item from the context to the toolbar.
toolbar.splice(4, 0, toolbarItem(tools.card))
return toolbar
},
keyboardShortcutsMenu(_app, keyboardShortcutsMenu, { tools }) {
// Add the tool item from the context to the keyboard shortcuts dialog.
const toolsGroup = keyboardShortcutsMenu.find(
(group) => group.id === 'shortcuts-dialog.tools'
) as TLUiMenuGroup
toolsGroup.children.push(menuItem(tools.card))
return keyboardShortcutsMenu
},
}

View file

@ -6,7 +6,7 @@ export class ErrorShapeUtil extends BaseBoxShapeUtil<ErrorShape> {
static override type = 'error' as const static override type = 'error' as const
override type = 'error' as const override type = 'error' as const
defaultProps() { getDefaultProps() {
return { message: 'Error!', w: 100, h: 100 } return { message: 'Error!', w: 100, h: 100 }
} }
component(shape: ErrorShape) { component(shape: ErrorShape) {

View file

@ -12,6 +12,7 @@ import { Box2dModel } from '@tldraw/tlschema';
import { Computed } from 'signia'; import { Computed } from 'signia';
import { ComputedCache } from '@tldraw/store'; import { ComputedCache } from '@tldraw/store';
import { CubicSpline2d } from '@tldraw/primitives'; import { CubicSpline2d } from '@tldraw/primitives';
import { defineMigrations } from '@tldraw/store';
import { EASINGS } from '@tldraw/primitives'; import { EASINGS } from '@tldraw/primitives';
import { EmbedDefinition } from '@tldraw/tlschema'; import { EmbedDefinition } from '@tldraw/tlschema';
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
@ -111,15 +112,13 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
// (undocumented) // (undocumented)
component(shape: TLArrowShape): JSX.Element | null; component(shape: TLArrowShape): JSX.Element | null;
// (undocumented) // (undocumented)
defaultProps(): TLArrowShape['props'];
// (undocumented)
getArrowInfo(shape: TLArrowShape): ArrowInfo | undefined; getArrowInfo(shape: TLArrowShape): ArrowInfo | undefined;
// (undocumented) // (undocumented)
getBounds(shape: TLArrowShape): Box2d; getBounds(shape: TLArrowShape): Box2d;
// (undocumented) // (undocumented)
getCenter(shape: TLArrowShape): Vec2d; getCenter(shape: TLArrowShape): Vec2d;
// (undocumented) // (undocumented)
getEditingBounds: (shape: TLArrowShape) => Box2d; getDefaultProps(): TLArrowShape['props'];
// (undocumented) // (undocumented)
getHandles(shape: TLArrowShape): TLHandle[]; getHandles(shape: TLArrowShape): TLHandle[];
// (undocumented) // (undocumented)
@ -205,7 +204,7 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
// (undocumented) // (undocumented)
component(shape: TLBookmarkShape): JSX.Element; component(shape: TLBookmarkShape): JSX.Element;
// (undocumented) // (undocumented)
defaultProps(): TLBookmarkShape['props']; getDefaultProps(): TLBookmarkShape['props'];
// (undocumented) // (undocumented)
hideSelectionBoundsBg: () => boolean; hideSelectionBoundsBg: () => boolean;
// (undocumented) // (undocumented)
@ -293,6 +292,8 @@ export const defaultShapes: readonly [TLShapeInfo<TLDrawShape>, TLShapeInfo<TLGe
// @public (undocumented) // @public (undocumented)
export const defaultTools: TLStateNodeConstructor[]; export const defaultTools: TLStateNodeConstructor[];
export { defineMigrations }
// @public (undocumented) // @public (undocumented)
export function defineShape<T extends TLUnknownShape>(type: T['type'], opts: Omit<TLShapeInfo<T>, 'type'>): TLShapeInfo<T>; export function defineShape<T extends TLUnknownShape>(type: T['type'], opts: Omit<TLShapeInfo<T>, 'type'>): TLShapeInfo<T>;
@ -313,14 +314,14 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
// (undocumented) // (undocumented)
component(shape: TLDrawShape): JSX.Element; component(shape: TLDrawShape): JSX.Element;
// (undocumented) // (undocumented)
defaultProps(): TLDrawShape['props'];
// (undocumented)
expandSelectionOutlinePx(shape: TLDrawShape): number; expandSelectionOutlinePx(shape: TLDrawShape): number;
// (undocumented) // (undocumented)
getBounds(shape: TLDrawShape): Box2d; getBounds(shape: TLDrawShape): Box2d;
// (undocumented) // (undocumented)
getCenter(shape: TLDrawShape): Vec2d; getCenter(shape: TLDrawShape): Vec2d;
// (undocumented) // (undocumented)
getDefaultProps(): TLDrawShape['props'];
// (undocumented)
getOutline(shape: TLDrawShape): Vec2d[]; getOutline(shape: TLDrawShape): Vec2d[];
// (undocumented) // (undocumented)
hideResizeHandles: (shape: TLDrawShape) => boolean; hideResizeHandles: (shape: TLDrawShape) => boolean;
@ -450,20 +451,24 @@ export class Editor extends EventEmitter<TLEventMap> {
}[]; }[];
getAssetById(id: TLAssetId): TLAsset | undefined; getAssetById(id: TLAssetId): TLAsset | undefined;
getAssetBySrc(src: string): TLBookmarkAsset | TLImageAsset | TLVideoAsset | undefined; getAssetBySrc(src: string): TLBookmarkAsset | TLImageAsset | TLVideoAsset | undefined;
getBounds(shape: TLShape): Box2d; getBounds<T extends TLShape>(shape: T): Box2d;
getBoundsById(id: TLShapeId): Box2d | undefined; getBoundsById<T extends TLShape>(id: T['id']): Box2d | undefined;
getClipPathById(id: TLShapeId): string | undefined; getClipPathById(id: TLShapeId): string | undefined;
getContainer: () => HTMLElement; getContainer: () => HTMLElement;
getContent(ids?: TLShapeId[]): TLContent | undefined; getContent(ids?: TLShapeId[]): TLContent | undefined;
getDeltaInParentSpace(shape: TLShape, delta: VecLike): Vec2d; getDeltaInParentSpace(shape: TLShape, delta: VecLike): Vec2d;
getDeltaInShapeSpace(shape: TLShape, delta: VecLike): Vec2d; getDeltaInShapeSpace(shape: TLShape, delta: VecLike): Vec2d;
getDroppingShape(point: VecLike, droppingShapes?: TLShape[]): TLUnknownShape | undefined; getDroppingShape(point: VecLike, droppingShapes?: TLShape[]): TLUnknownShape | undefined;
getHandles<T extends TLShape>(shape: T): TLHandle[] | undefined;
getHandlesById<T extends TLShape>(id: T['id']): TLHandle[] | undefined;
getHighestIndexForParent(parentId: TLPageId | TLShapeId): string; getHighestIndexForParent(parentId: TLPageId | TLShapeId): string;
getMaskedPageBounds(shape: TLShape): Box2d | undefined; getMaskedPageBounds(shape: TLShape): Box2d | undefined;
getMaskedPageBoundsById(id: TLShapeId): Box2d | undefined; getMaskedPageBoundsById(id: TLShapeId): Box2d | undefined;
getOutermostSelectableShape(shape: TLShape, filter?: (shape: TLShape) => boolean): TLShape; getOutermostSelectableShape(shape: TLShape, filter?: (shape: TLShape) => boolean): TLShape;
getOutline(shape: TLShape): Vec2d[]; getOutline<T extends TLShape>(shape: T): Vec2d[];
getOutlineById(id: TLShapeId): Vec2d[]; getOutlineById(id: TLShapeId): Vec2d[];
getOutlineSegments<T extends TLShape>(shape: T): Vec2d[][];
getOutlineSegmentsById(id: TLShapeId): Vec2d[][];
getPageBounds(shape: TLShape): Box2d | undefined; getPageBounds(shape: TLShape): Box2d | undefined;
getPageBoundsById(id: TLShapeId): Box2d | undefined; getPageBoundsById(id: TLShapeId): Box2d | undefined;
getPageById(id: TLPageId): TLPage | undefined; getPageById(id: TLPageId): TLPage | undefined;
@ -644,6 +649,7 @@ export class Editor extends EventEmitter<TLEventMap> {
setCurrentPageId(pageId: TLPageId, { stopFollowing }?: TLViewportOptions): this; setCurrentPageId(pageId: TLPageId, { stopFollowing }?: TLViewportOptions): this;
setCursor(cursor: Partial<TLCursor>): this; setCursor(cursor: Partial<TLCursor>): this;
setDarkMode(isDarkMode: boolean): this; setDarkMode(isDarkMode: boolean): this;
setDevicePixelRatio(dpr: number): this;
setEditingId(id: null | TLShapeId): this; setEditingId(id: null | TLShapeId): this;
setErasingIds(ids?: TLShapeId[]): this; setErasingIds(ids?: TLShapeId[]): this;
setFocusLayer(next: null | TLShapeId): this; setFocusLayer(next: null | TLShapeId): this;
@ -651,9 +657,9 @@ export class Editor extends EventEmitter<TLEventMap> {
setGridMode(isGridMode: boolean): this; setGridMode(isGridMode: boolean): this;
setHintingIds(ids: TLShapeId[]): this; setHintingIds(ids: TLShapeId[]): this;
setHoveredId(id?: null | TLShapeId): this; setHoveredId(id?: null | TLShapeId): this;
setInstancePageState(partial: Partial<TLInstancePageState>, ephemeral?: boolean): void;
setLocale(locale: string): void; setLocale(locale: string): void;
setOpacity(opacity: number, ephemeral?: boolean, squashing?: boolean): this; setOpacity(opacity: number, ephemeral?: boolean, squashing?: boolean): this;
setPageState(partial: Partial<TLInstancePageState>, ephemeral?: boolean): void;
setPenMode(isPenMode: boolean): this; setPenMode(isPenMode: boolean): this;
// @internal (undocumented) // @internal (undocumented)
setProjectName(name: string): void; setProjectName(name: string): void;
@ -685,7 +691,7 @@ export class Editor extends EventEmitter<TLEventMap> {
stopFollowingUser(): this; stopFollowingUser(): this;
readonly store: TLStore; readonly store: TLStore;
stretchShapes(operation: 'horizontal' | 'vertical', ids?: TLShapeId[]): this; stretchShapes(operation: 'horizontal' | 'vertical', ids?: TLShapeId[]): this;
textMeasure: TextManager; readonly textMeasure: TextManager;
toggleLock(ids?: TLShapeId[]): this; toggleLock(ids?: TLShapeId[]): this;
undo(): HistoryManager<this>; undo(): HistoryManager<this>;
ungroupShapes(ids?: TLShapeId[]): this; ungroupShapes(ids?: TLShapeId[]): this;
@ -727,7 +733,7 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
// (undocumented) // (undocumented)
component(shape: TLEmbedShape): JSX.Element; component(shape: TLEmbedShape): JSX.Element;
// (undocumented) // (undocumented)
defaultProps(): TLEmbedShape['props']; getDefaultProps(): TLEmbedShape['props'];
// (undocumented) // (undocumented)
hideSelectionBoundsBg: TLShapeUtilFlag<TLEmbedShape>; hideSelectionBoundsBg: TLShapeUtilFlag<TLEmbedShape>;
// (undocumented) // (undocumented)
@ -792,7 +798,7 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
// (undocumented) // (undocumented)
component(shape: TLFrameShape): JSX.Element; component(shape: TLFrameShape): JSX.Element;
// (undocumented) // (undocumented)
defaultProps(): TLFrameShape['props']; getDefaultProps(): TLFrameShape['props'];
// (undocumented) // (undocumented)
indicator(shape: TLFrameShape): JSX.Element; indicator(shape: TLFrameShape): JSX.Element;
// (undocumented) // (undocumented)
@ -821,12 +827,12 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
// (undocumented) // (undocumented)
component(shape: TLGeoShape): JSX.Element; component(shape: TLGeoShape): JSX.Element;
// (undocumented) // (undocumented)
defaultProps(): TLGeoShape['props'];
// (undocumented)
getBounds(shape: TLGeoShape): Box2d; getBounds(shape: TLGeoShape): Box2d;
// (undocumented) // (undocumented)
getCenter(shape: TLGeoShape): Vec2d; getCenter(shape: TLGeoShape): Vec2d;
// (undocumented) // (undocumented)
getDefaultProps(): TLGeoShape['props'];
// (undocumented)
getOutline(shape: TLGeoShape): Vec2d[]; getOutline(shape: TLGeoShape): Vec2d[];
// (undocumented) // (undocumented)
hitTestLineSegment(shape: TLGeoShape, A: VecLike, B: VecLike): boolean; hitTestLineSegment(shape: TLGeoShape, A: VecLike, B: VecLike): boolean;
@ -1041,12 +1047,12 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
// (undocumented) // (undocumented)
component(shape: TLGroupShape): JSX.Element | null; component(shape: TLGroupShape): JSX.Element | null;
// (undocumented) // (undocumented)
defaultProps(): TLGroupShape['props'];
// (undocumented)
getBounds(shape: TLGroupShape): Box2d; getBounds(shape: TLGroupShape): Box2d;
// (undocumented) // (undocumented)
getCenter(shape: TLGroupShape): Vec2d; getCenter(shape: TLGroupShape): Vec2d;
// (undocumented) // (undocumented)
getDefaultProps(): TLGroupShape['props'];
// (undocumented)
getOutline(shape: TLGroupShape): Vec2d[]; getOutline(shape: TLGroupShape): Vec2d[];
// (undocumented) // (undocumented)
hideSelectionBoundsBg: () => boolean; hideSelectionBoundsBg: () => boolean;
@ -1082,9 +1088,9 @@ export const HighlightShape: TLShapeInfo<TLHighlightShape>;
// @public (undocumented) // @public (undocumented)
export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> { export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
// (undocumented) // (undocumented)
component(shape: TLHighlightShape): JSX.Element; backgroundComponent(shape: TLHighlightShape): JSX.Element;
// (undocumented) // (undocumented)
defaultProps(): TLHighlightShape['props']; component(shape: TLHighlightShape): JSX.Element;
// (undocumented) // (undocumented)
expandSelectionOutlinePx(shape: TLHighlightShape): number; expandSelectionOutlinePx(shape: TLHighlightShape): number;
// (undocumented) // (undocumented)
@ -1092,6 +1098,8 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
// (undocumented) // (undocumented)
getCenter(shape: TLHighlightShape): Vec2d; getCenter(shape: TLHighlightShape): Vec2d;
// (undocumented) // (undocumented)
getDefaultProps(): TLHighlightShape['props'];
// (undocumented)
getOutline(shape: TLHighlightShape): Vec2d[]; getOutline(shape: TLHighlightShape): Vec2d[];
// (undocumented) // (undocumented)
hideResizeHandles: (shape: TLHighlightShape) => boolean; hideResizeHandles: (shape: TLHighlightShape) => boolean;
@ -1110,8 +1118,6 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
// (undocumented) // (undocumented)
onResize: TLOnResizeHandler<TLHighlightShape>; onResize: TLOnResizeHandler<TLHighlightShape>;
// (undocumented) // (undocumented)
renderBackground(shape: TLHighlightShape): JSX.Element;
// (undocumented)
toBackgroundSvg(shape: TLHighlightShape, font: string | undefined, colors: TLExportColors): SVGPathElement; toBackgroundSvg(shape: TLHighlightShape, font: string | undefined, colors: TLExportColors): SVGPathElement;
// (undocumented) // (undocumented)
toSvg(shape: TLHighlightShape, _font: string | undefined, colors: TLExportColors): SVGPathElement; toSvg(shape: TLHighlightShape, _font: string | undefined, colors: TLExportColors): SVGPathElement;
@ -1135,7 +1141,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
// (undocumented) // (undocumented)
component(shape: TLImageShape): JSX.Element; component(shape: TLImageShape): JSX.Element;
// (undocumented) // (undocumented)
defaultProps(): TLImageShape['props']; getDefaultProps(): TLImageShape['props'];
// (undocumented) // (undocumented)
indicator(shape: TLImageShape): JSX.Element | null; indicator(shape: TLImageShape): JSX.Element | null;
// (undocumented) // (undocumented)
@ -1182,11 +1188,9 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
// (undocumented) // (undocumented)
component(shape: TLLineShape): JSX.Element | undefined; component(shape: TLLineShape): JSX.Element | undefined;
// (undocumented) // (undocumented)
defaultProps(): TLLineShape['props'];
// (undocumented)
getBounds(shape: TLLineShape): Box2d; getBounds(shape: TLLineShape): Box2d;
// (undocumented) // (undocumented)
getCenter(shape: TLLineShape): Vec2d; getDefaultProps(): TLLineShape['props'];
// (undocumented) // (undocumented)
getHandles(shape: TLLineShape): TLHandle[]; getHandles(shape: TLLineShape): TLHandle[];
// (undocumented) // (undocumented)
@ -1650,12 +1654,12 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
// (undocumented) // (undocumented)
component(shape: TLNoteShape): JSX.Element; component(shape: TLNoteShape): JSX.Element;
// (undocumented) // (undocumented)
defaultProps(): TLNoteShape['props'];
// (undocumented)
getBounds(shape: TLNoteShape): Box2d; getBounds(shape: TLNoteShape): Box2d;
// (undocumented) // (undocumented)
getCenter(_shape: TLNoteShape): Vec2d; getCenter(_shape: TLNoteShape): Vec2d;
// (undocumented) // (undocumented)
getDefaultProps(): TLNoteShape['props'];
// (undocumented)
getHeight(shape: TLNoteShape): number; getHeight(shape: TLNoteShape): number;
// (undocumented) // (undocumented)
getOutline(shape: TLNoteShape): Vec2d[]; getOutline(shape: TLNoteShape): Vec2d[];
@ -1820,7 +1824,8 @@ export function setUserPreferences(user: TLUserPreferences): void;
// @public (undocumented) // @public (undocumented)
export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> { export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
constructor(editor: Editor, type: Shape['type'], styleProps: ReadonlyMap<StyleProp<unknown>, string>); constructor(editor: Editor, type: Shape['type'], styleProps: ReadonlyMap<StyleProp<unknown>, string>);
bounds(shape: Shape): Box2d; // @internal
backgroundComponent?(shape: Shape): any;
canBind: <K>(_shape: Shape, _otherShape?: K | undefined) => boolean; canBind: <K>(_shape: Shape, _otherShape?: K | undefined) => boolean;
canCrop: TLShapeUtilFlag<Shape>; canCrop: TLShapeUtilFlag<Shape>;
canDropShapes(shape: Shape, shapes: TLShape[]): boolean; canDropShapes(shape: Shape, shapes: TLShape[]): boolean;
@ -1832,20 +1837,18 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
canUnmount: TLShapeUtilFlag<Shape>; canUnmount: TLShapeUtilFlag<Shape>;
center(shape: Shape): Vec2d; center(shape: Shape): Vec2d;
abstract component(shape: Shape): any; abstract component(shape: Shape): any;
abstract defaultProps(): Shape['props'];
// (undocumented) // (undocumented)
editor: Editor; editor: Editor;
// @internal (undocumented) // @internal (undocumented)
expandSelectionOutlinePx(shape: Shape): number; expandSelectionOutlinePx(shape: Shape): number;
protected abstract getBounds(shape: Shape): Box2d; abstract getBounds(shape: Shape): Box2d;
abstract getCenter(shape: Shape): Vec2d; getCenter(shape: Shape): Vec2d;
getEditingBounds: (shape: Shape) => Box2d; abstract getDefaultProps(): Shape['props'];
protected getHandles?(shape: Shape): TLHandle[]; getHandles?(shape: Shape): TLHandle[];
protected abstract getOutline(shape: Shape): Vec2d[]; getOutline(shape: Shape): Vec2d[];
protected getOutlineSegments(shape: Shape): Vec2d[][]; getOutlineSegments(shape: Shape): Vec2d[][];
// (undocumented) // (undocumented)
getStyleIfExists<T>(style: StyleProp<T>, shape: Shape | TLShapePartial<Shape>): T | undefined; getStyleIfExists<T>(style: StyleProp<T>, shape: Shape | TLShapePartial<Shape>): T | undefined;
handles(shape: Shape): TLHandle[];
// (undocumented) // (undocumented)
hasStyle(style: StyleProp<unknown>): boolean; hasStyle(style: StyleProp<unknown>): boolean;
hideResizeHandles: TLShapeUtilFlag<Shape>; hideResizeHandles: TLShapeUtilFlag<Shape>;
@ -1884,12 +1887,8 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
onTranslate?: TLOnTranslateHandler<Shape>; onTranslate?: TLOnTranslateHandler<Shape>;
onTranslateEnd?: TLOnTranslateEndHandler<Shape>; onTranslateEnd?: TLOnTranslateEndHandler<Shape>;
onTranslateStart?: TLOnTranslateStartHandler<Shape>; onTranslateStart?: TLOnTranslateStartHandler<Shape>;
outline(shape: Shape): Vec2d[];
outlineSegments(shape: Shape): Vec2d[][];
// @internal // @internal
providesBackgroundForChildren(shape: Shape): boolean; providesBackgroundForChildren(shape: Shape): boolean;
// @internal
renderBackground?(shape: Shape): any;
// (undocumented) // (undocumented)
setStyleInPartial<T>(style: StyleProp<T>, shape: TLShapePartial<Shape>, value: T): TLShapePartial<Shape>; setStyleInPartial<T>(style: StyleProp<T>, shape: TLShapePartial<Shape>, value: T): TLShapePartial<Shape>;
snapPoints(shape: Shape): Vec2d[]; snapPoints(shape: Shape): Vec2d[];
@ -2020,11 +2019,9 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
// (undocumented) // (undocumented)
component(shape: TLTextShape): JSX.Element; component(shape: TLTextShape): JSX.Element;
// (undocumented) // (undocumented)
defaultProps(): TLTextShape['props'];
// (undocumented)
getBounds(shape: TLTextShape): Box2d; getBounds(shape: TLTextShape): Box2d;
// (undocumented) // (undocumented)
getCenter(shape: TLTextShape): Vec2d; getDefaultProps(): TLTextShape['props'];
// (undocumented) // (undocumented)
getMinDimensions(shape: TLTextShape): { getMinDimensions(shape: TLTextShape): {
height: number; height: number;
@ -2489,7 +2486,7 @@ export type TLOnHandleChangeHandler<T extends TLShape> = (shape: T, info: {
export type TLOnResizeEndHandler<T extends TLShape> = TLEventChangeHandler<T>; export type TLOnResizeEndHandler<T extends TLShape> = TLEventChangeHandler<T>;
// @public (undocumented) // @public (undocumented)
export type TLOnResizeHandler<T extends TLShape> = (shape: T, info: TLResizeInfo<T>) => Partial<TLShapePartial<T>> | undefined | void; export type TLOnResizeHandler<T extends TLShape> = (shape: T, info: TLResizeInfo<T>) => Omit<TLShapePartial<T>, 'id' | 'type'> | undefined | void;
// @public (undocumented) // @public (undocumented)
export type TLOnResizeStartHandler<T extends TLShape> = TLEventStartHandler<T>; export type TLOnResizeStartHandler<T extends TLShape> = TLEventStartHandler<T>;
@ -2763,7 +2760,7 @@ export class VideoShapeUtil extends BaseBoxShapeUtil<TLVideoShape> {
// (undocumented) // (undocumented)
component(shape: TLVideoShape): JSX.Element; component(shape: TLVideoShape): JSX.Element;
// (undocumented) // (undocumented)
defaultProps(): TLVideoShape['props']; getDefaultProps(): TLVideoShape['props'];
// (undocumented) // (undocumented)
indicator(shape: TLVideoShape): JSX.Element; indicator(shape: TLVideoShape): JSX.Element;
// (undocumented) // (undocumented)

View file

@ -3,6 +3,7 @@
// eslint-disable-next-line local/no-export-star // eslint-disable-next-line local/no-export-star
export * from '@tldraw/indices' export * from '@tldraw/indices'
export { defineMigrations } from '@tldraw/store'
// eslint-disable-next-line local/no-export-star // eslint-disable-next-line local/no-export-star
export * from '@tldraw/tlschema' export * from '@tldraw/tlschema'
export { getHashForString } from '@tldraw/utils' export { getHashForString } from '@tldraw/utils'

View file

@ -207,8 +207,7 @@ const HandlesWrapper = track(function HandlesWrapper() {
if (!(onlySelectedShape && shouldDisplayHandles)) return null if (!(onlySelectedShape && shouldDisplayHandles)) return null
const util = editor.getShapeUtil(onlySelectedShape) const handles = editor.getHandles(onlySelectedShape)
const handles = util.handles?.(onlySelectedShape)
if (!handles) return null if (!handles) return null

View file

@ -83,8 +83,7 @@ export const Shape = track(function Shape({
const shape = editor.getShapeById(id) const shape = editor.getShapeById(id)
if (!shape) return null if (!shape) return null
const util = editor.getShapeUtil(shape) const bounds = editor.getBounds(shape)
const bounds = util.bounds(shape)
setProperty('width', Math.ceil(bounds.width) + 'px') setProperty('width', Math.ceil(bounds.width) + 'px')
setProperty('height', Math.ceil(bounds.height) + 'px') setProperty('height', Math.ceil(bounds.height) + 'px')
}, },
@ -106,7 +105,7 @@ export const Shape = track(function Shape({
return ( return (
<> <>
{util.renderBackground && ( {util.backgroundComponent && (
<div <div
ref={backgroundContainerRef} ref={backgroundContainerRef}
className="tl-shape tl-shape-background" className="tl-shape tl-shape-background"
@ -137,7 +136,7 @@ export const Shape = track(function Shape({
onPointerLeave={events.onPointerLeave} onPointerLeave={events.onPointerLeave}
> >
{isCulled && util.canUnmount(shape) ? ( {isCulled && util.canUnmount(shape) ? (
<CulledShape shape={shape} util={util} /> <CulledShape shape={shape} />
) : ( ) : (
<OptionalErrorBoundary <OptionalErrorBoundary
fallback={ShapeErrorFallback ? (error) => <ShapeErrorFallback error={error} /> : null} fallback={ShapeErrorFallback ? (error) => <ShapeErrorFallback error={error} /> : null}
@ -168,14 +167,16 @@ const InnerShapeBackground = React.memo(
shape: T shape: T
util: ShapeUtil<T> util: ShapeUtil<T>
}) { }) {
return useStateTracking('InnerShape:' + util.type, () => util.renderBackground?.(shape)) return useStateTracking('InnerShape:' + util.type, () => util.backgroundComponent?.(shape))
}, },
(prev, next) => prev.shape.props === next.shape.props (prev, next) => prev.shape.props === next.shape.props
) )
const CulledShape = React.memo( const CulledShape = React.memo(
function CulledShap<T extends TLShape>({ shape, util }: { shape: T; util: ShapeUtil<T> }) { function CulledShape<T extends TLShape>({ shape }: { shape: T }) {
const bounds = util.bounds(shape) const editor = useEditor()
const bounds = editor.getBounds(shape)
return ( return (
<div <div
className="tl-shape__culled" className="tl-shape__culled"

File diff suppressed because it is too large Load diff

View file

@ -1,68 +0,0 @@
import { atom } from 'signia'
import { Editor } from '../Editor'
type Offsets = {
top: number
left: number
bottom: number
right: number
}
const DEFAULT_OFFSETS = {
top: 10,
left: 10,
bottom: 10,
right: 10,
}
export function getActiveAreaScreenSpace(editor: Editor) {
const containerEl = editor.getContainer()
const el = containerEl.querySelector('*[data-tldraw-area="active-drawing"]')
const out = {
...DEFAULT_OFFSETS,
width: 0,
height: 0,
}
if (el && containerEl) {
const cBbbox = containerEl.getBoundingClientRect()
const bbox = el.getBoundingClientRect()
out.top = bbox.top
out.left = bbox.left
out.bottom = cBbbox.height - bbox.bottom
out.right = cBbbox.width - bbox.right
}
out.width = editor.viewportScreenBounds.width - out.left - out.right
out.height = editor.viewportScreenBounds.height - out.top - out.bottom
return out
}
export function getActiveAreaPageSpace(editor: Editor) {
const out = getActiveAreaScreenSpace(editor)
const z = editor.zoomLevel
out.left /= z
out.right /= z
out.top /= z
out.bottom /= z
out.width /= z
out.height /= z
return out
}
export class ActiveAreaManager {
constructor(public editor: Editor) {
window.addEventListener('resize', this.updateOffsets)
this.editor.disposables.add(this.dispose)
}
offsets = atom<Offsets>('activeAreaOffsets', DEFAULT_OFFSETS)
updateOffsets = () => {
const offsets = getActiveAreaPageSpace(this.editor)
this.offsets.set(offsets)
}
// Clear the listener
dispose = () => {
window.addEventListener('resize', this.updateOffsets)
}
}

View file

@ -1,45 +0,0 @@
import { atom } from 'signia'
import { Editor } from '../Editor'
export class DprManager {
private _currentMM: MediaQueryList | undefined
constructor(public editor: Editor) {
this.rebind()
// Add this class's dispose method (cancel the listener) to the app's disposables
this.editor.disposables.add(this.dispose)
}
// Set a listener to update the dpr when the device pixel ratio changes
rebind() {
this.dispose()
this._currentMM = this.getMedia()
this._currentMM?.addEventListener('change', this.updateDevicePixelRatio)
}
dpr = atom<number>(
'devicePixelRatio',
typeof window === 'undefined' ? 1 : window.devicePixelRatio
)
// Get the media query list for the device pixel ratio
getMedia() {
// NOTE: This ignore is only for the test environment.
/* @ts-ignore */
if (window.matchMedia) {
return matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
}
}
// Update the device pixel ratio atom
updateDevicePixelRatio = () => {
this.dpr.set(window.devicePixelRatio)
this.rebind()
}
// Clear the listener
dispose = () => {
this._currentMM?.removeEventListener('change', this.updateDevicePixelRatio)
}
}

View file

@ -235,7 +235,7 @@ export class ExternalContentManager {
const p = const p =
point ?? (editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.viewportPageCenter) point ?? (editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.viewportPageCenter)
const defaultProps = editor.getShapeUtil(TextShapeUtil).defaultProps() const defaultProps = editor.getShapeUtil(TextShapeUtil).getDefaultProps()
const textToPaste = stripTrailingWhitespace( const textToPaste = stripTrailingWhitespace(
stripCommonMinimumIndentation(replaceTabsWithSpaces(text)) stripCommonMinimumIndentation(replaceTabsWithSpaces(text))

View file

@ -17,15 +17,15 @@ export abstract class BaseBoxShapeUtil<Shape extends TLBaseBoxShape> extends Sha
} }
override getOutline(shape: Shape) { override getOutline(shape: Shape) {
return this.bounds(shape).corners return this.editor.getBounds(shape).corners
} }
hitTestPoint(shape: Shape, point: VecLike): boolean { override hitTestPoint(shape: Shape, point: VecLike): boolean {
return pointInPolygon(point, this.outline(shape)) return pointInPolygon(point, this.editor.getOutline(shape))
} }
hitTestLineSegment(shape: Shape, A: VecLike, B: VecLike): boolean { override hitTestLineSegment(shape: Shape, A: VecLike, B: VecLike): boolean {
const outline = this.outline(shape) const outline = this.editor.getOutline(shape)
for (let i = 0; i < outline.length; i++) { for (let i = 0; i < outline.length; i++) {
const C = outline[i] const C = outline[i]
@ -36,7 +36,7 @@ export abstract class BaseBoxShapeUtil<Shape extends TLBaseBoxShape> extends Sha
return false return false
} }
onResize: TLOnResizeHandler<any> = (shape, info) => { override onResize: TLOnResizeHandler<any> = (shape, info) => {
return resizeBox(shape, info) return resizeBox(shape, info)
} }
} }

View file

@ -1,8 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import { Box2d, linesIntersect, Vec2d, VecLike } from '@tldraw/primitives' import { Box2d, linesIntersect, Vec2d, VecLike } from '@tldraw/primitives'
import { ComputedCache } from '@tldraw/store'
import { StyleProp, TLHandle, TLShape, TLShapePartial, TLUnknownShape } from '@tldraw/tlschema' import { StyleProp, TLHandle, TLShape, TLShapePartial, TLUnknownShape } from '@tldraw/tlschema'
import { computed, EMPTY_ARRAY } from 'signia'
import type { Editor } from '../Editor' import type { Editor } from '../Editor'
import { TLResizeHandle } from '../types/selection-types' import { TLResizeHandle } from '../types/selection-types'
import { TLExportColors } from './shared/TLExportColors' import { TLExportColors } from './shared/TLExportColors'
@ -67,6 +65,29 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
*/ */
static type: string static type: string
/**
* Get the default props for a shape.
*
* @public
*/
abstract getDefaultProps(): Shape['props']
/**
* Get a JSX element for the shape (as an HTML element).
*
* @param shape - The shape.
* @public
*/
abstract component(shape: Shape): any
/**
* Get JSX describing the shape's indicator (as an SVG element).
*
* @param shape - The shape.
* @public
*/
abstract indicator(shape: Shape): any
/** /**
* Whether the shape can be snapped to by another shape. * Whether the shape can be snapped to by another shape.
* *
@ -118,14 +139,16 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
canCrop: TLShapeUtilFlag<Shape> = () => false canCrop: TLShapeUtilFlag<Shape> = () => false
/** /**
* Bounds of the shape to edit. * Does this shape provide a background for its children? If this is true,
* then any children with a `renderBackground` method will have their
* backgrounds rendered _above_ this shape. Otherwise, the children's
* backgrounds will be rendered above either the next ancestor that provides
* a background, or the canvas background.
* *
* Note: this could be a text area within a shape for example arrow labels. * @internal
*
* @public
*/ */
getEditingBounds = (shape: Shape) => { providesBackgroundForChildren(shape: Shape): boolean {
return this.bounds(shape) return false
} }
/** /**
@ -170,36 +193,13 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
*/ */
isAspectRatioLocked: TLShapeUtilFlag<Shape> = () => false isAspectRatioLocked: TLShapeUtilFlag<Shape> = () => false
/**
* Get the default props for a shape.
*
* @public
*/
abstract defaultProps(): Shape['props']
/**
* Get a JSX element for the shape (as an HTML element).
*
* @param shape - The shape.
* @public
*/
abstract component(shape: Shape): any
/**
* Get JSX describing the shape's indicator (as an SVG element).
*
* @param shape - The shape.
* @public
*/
abstract indicator(shape: Shape): any
/** /**
* Get a JSX element for the shape (as an HTML element) to be rendered as part of the canvas background - behind any other shape content. * Get a JSX element for the shape (as an HTML element) to be rendered as part of the canvas background - behind any other shape content.
* *
* @param shape - The shape. * @param shape - The shape.
* @internal * @internal
*/ */
renderBackground?(shape: Shape): any backgroundComponent?(shape: Shape): any
/** /**
* Get an array of handle models for the shape. This is an optional method. * Get an array of handle models for the shape. This is an optional method.
@ -213,25 +213,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
* @param shape - The shape. * @param shape - The shape.
* @public * @public
*/ */
protected getHandles?(shape: Shape): TLHandle[] getHandles?(shape: Shape): TLHandle[]
@computed
private get handlesCache(): ComputedCache<TLHandle[], TLShape> {
return this.editor.store.createComputedCache('handles:' + this.type, (shape) => {
return this.getHandles!(shape as any)
})
}
/**
* Get the cached handles (this should not be overridden!)
*
* @param shape - The shape.
* @public
*/
handles(shape: Shape): TLHandle[] {
if (!this.getHandles) return EMPTY_ARRAY
return this.handlesCache.get(shape.id) ?? EMPTY_ARRAY
}
/** /**
* Get an array of outline segments for the shape. For most shapes, * Get an array of outline segments for the shape. For most shapes,
@ -248,26 +230,8 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
* @param shape - The shape. * @param shape - The shape.
* @public * @public
*/ */
protected getOutlineSegments(shape: Shape): Vec2d[][] { getOutlineSegments(shape: Shape): Vec2d[][] {
return [this.outline(shape)] return [this.editor.getOutline(shape)]
}
@computed
private get outlineSegmentsCache(): ComputedCache<Vec2d[][], TLShape> {
return this.editor.store.createComputedCache('outline-segments:' + this.type, (shape) => {
return this.getOutlineSegments!(shape as any)
})
}
/**
* Get the cached outline segments (this should not be overridden!)
*
* @param shape - The shape.
* @public
*/
outlineSegments(shape: Shape): Vec2d[][] {
if (!this.getOutlineSegments) return EMPTY_ARRAY
return this.outlineSegmentsCache.get(shape.id) ?? EMPTY_ARRAY
} }
/** /**
@ -276,52 +240,16 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
* @param shape - The shape. * @param shape - The shape.
* @public * @public
*/ */
protected abstract getBounds(shape: Shape): Box2d abstract getBounds(shape: Shape): Box2d
@computed
private get boundsCache(): ComputedCache<Box2d, TLShape> {
return this.editor.store.createComputedCache('bounds:' + this.type, (shape) => {
return this.getBounds(shape as any)
})
}
/** /**
* Get the cached bounds for the shape. * Get the shape's (not cached) outline.
* *
* @param shape - The shape. * @param shape - The shape.
* @public * @public
*/ */
bounds(shape: Shape): Box2d { getOutline(shape: Shape): Vec2d[] {
const result = this.boundsCache.get(shape.id) ?? new Box2d() return this.editor.getBounds(shape).corners
if (result.width === 0 || result.height === 0) {
return new Box2d(result.x, result.y, Math.max(result.width, 1), Math.max(result.height, 1))
}
return result
}
/**
* Get the shape's (not cached) outline. Do not override this method!
*
* @param shape - The shape.
* @public
*/
protected abstract getOutline(shape: Shape): Vec2d[]
@computed
private get outlineCache(): ComputedCache<Vec2d[], TLShape> {
return this.editor.store.createComputedCache('outline:' + this.type, (shape) => {
return this.getOutline(shape as any)
})
}
/**
* Get the shape's outline. Do not override this method!
*
* @param shape - The shape.
* @public
*/
outline(shape: Shape): Vec2d[] {
return this.outlineCache.get(shape.id) ?? EMPTY_ARRAY
} }
/** /**
@ -331,7 +259,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
* @public * @public
*/ */
snapPoints(shape: Shape) { snapPoints(shape: Shape) {
return this.bounds(shape).snapPoints return this.editor.getBounds(shape).snapPoints
} }
/** /**
@ -350,7 +278,9 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
* @param shape - The shape. * @param shape - The shape.
* @public * @public
*/ */
abstract getCenter(shape: Shape): Vec2d getCenter(shape: Shape) {
return this.editor.getBounds(shape).center
}
/** /**
* Get whether the shape can receive children of a given type. * Get whether the shape can receive children of a given type.
@ -403,6 +333,11 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
colors: TLExportColors colors: TLExportColors
): SVGElement | Promise<SVGElement> | null ): SVGElement | Promise<SVGElement> | null
/** @internal */
expandSelectionOutlinePx(shape: Shape): number {
return 0
}
/** /**
* Get whether a point intersects the shape. * Get whether a point intersects the shape.
* *
@ -412,7 +347,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
* @public * @public
*/ */
hitTestPoint(shape: Shape, point: VecLike): boolean { hitTestPoint(shape: Shape, point: VecLike): boolean {
return this.bounds(shape).containsPoint(point) return this.editor.getBounds(shape).containsPoint(point)
} }
/** /**
@ -425,7 +360,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
* @public * @public
*/ */
hitTestLineSegment(shape: Shape, A: VecLike, B: VecLike): boolean { hitTestLineSegment(shape: Shape, A: VecLike, B: VecLike): boolean {
const outline = this.outline(shape) const outline = this.editor.getOutline(shape)
for (let i = 0; i < outline.length; i++) { for (let i = 0; i < outline.length; i++) {
const C = outline[i] const C = outline[i]
@ -436,24 +371,6 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
return false return false
} }
/** @internal */
expandSelectionOutlinePx(shape: Shape): number {
return 0
}
/**
* Does this shape provide a background for its children? If this is true,
* then any children with a `renderBackground` method will have their
* backgrounds rendered _above_ this shape. Otherwise, the children's
* backgrounds will be rendered above either the next ancestor that provides
* a background, or the canvas background.
*
* @internal
*/
providesBackgroundForChildren(shape: Shape): boolean {
return false
}
// Events // Events
/** /**
@ -745,7 +662,7 @@ export type TLResizeInfo<T extends TLShape> = {
export type TLOnResizeHandler<T extends TLShape> = ( export type TLOnResizeHandler<T extends TLShape> = (
shape: T, shape: T,
info: TLResizeInfo<T> info: TLResizeInfo<T>
) => Partial<TLShapePartial<T>> | undefined | void ) => Omit<TLShapePartial<T>, 'id' | 'type'> | undefined | void
/** @public */ /** @public */
export type TLOnResizeStartHandler<T extends TLShape> = TLEventStartHandler<T> export type TLOnResizeStartHandler<T extends TLShape> = TLEventStartHandler<T>

View file

@ -73,7 +73,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
override hideSelectionBoundsFg: TLShapeUtilFlag<TLArrowShape> = () => true override hideSelectionBoundsFg: TLShapeUtilFlag<TLArrowShape> = () => true
override hideSelectionBoundsBg: TLShapeUtilFlag<TLArrowShape> = () => true override hideSelectionBoundsBg: TLShapeUtilFlag<TLArrowShape> = () => true
override defaultProps(): TLArrowShape['props'] { override getDefaultProps(): TLArrowShape['props'] {
return { return {
dash: 'draw', dash: 'draw',
size: 'm', size: 'm',
@ -91,7 +91,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
} }
getCenter(shape: TLArrowShape): Vec2d { getCenter(shape: TLArrowShape): Vec2d {
return this.bounds(shape).center return this.editor.getBounds(shape).center
} }
getBounds(shape: TLArrowShape) { getBounds(shape: TLArrowShape) {
@ -292,7 +292,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
if (util.isClosed(hitShape)) { if (util.isClosed(hitShape)) {
// Test the polygon // Test the polygon
return pointInPolygon(pointInTargetSpace, util.outline(hitShape)) return pointInPolygon(pointInTargetSpace, this.editor.getOutline(hitShape))
} }
// Test the point using the shape's idea of what a hit is // Test the point using the shape's idea of what a hit is
@ -533,7 +533,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
} }
hitTestPoint(shape: TLArrowShape, point: VecLike): boolean { hitTestPoint(shape: TLArrowShape, point: VecLike): boolean {
const outline = this.outline(shape) const outline = this.editor.getOutline(shape)
const zoomLevel = this.editor.zoomLevel const zoomLevel = this.editor.zoomLevel
const offsetDist = STROKE_SIZES[shape.props.size] / zoomLevel const offsetDist = STROKE_SIZES[shape.props.size] / zoomLevel
@ -548,7 +548,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
} }
hitTestLineSegment(shape: TLArrowShape, A: VecLike, B: VecLike): boolean { hitTestLineSegment(shape: TLArrowShape, A: VecLike, B: VecLike): boolean {
const outline = this.outline(shape) const outline = this.editor.getOutline(shape)
for (let i = 0; i < outline.length - 1; i++) { for (let i = 0; i < outline.length - 1; i++) {
const C = outline[i] const C = outline[i]
@ -571,7 +571,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
) && !this.editor.isReadOnly ) && !this.editor.isReadOnly
const info = this.getArrowInfo(shape) const info = this.getArrowInfo(shape)
const bounds = this.bounds(shape) const bounds = this.editor.getBounds(shape)
const labelSize = this.getLabelBounds(shape) const labelSize = this.getLabelBounds(shape)
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks
@ -750,7 +750,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
const { start, end } = getArrowTerminalsInArrowSpace(this.editor, shape) const { start, end } = getArrowTerminalsInArrowSpace(this.editor, shape)
const info = this.getArrowInfo(shape) const info = this.getArrowInfo(shape)
const bounds = this.bounds(shape) const bounds = this.editor.getBounds(shape)
const labelSize = this.getLabelBounds(shape) const labelSize = this.getLabelBounds(shape)
if (!info) return null if (!info) return null
@ -844,7 +844,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
@computed get labelBoundsCache(): ComputedCache<Box2d | null, TLArrowShape> { @computed get labelBoundsCache(): ComputedCache<Box2d | null, TLArrowShape> {
return this.editor.store.createComputedCache('labelBoundsCache', (shape) => { return this.editor.store.createComputedCache('labelBoundsCache', (shape) => {
const info = this.getArrowInfo(shape) const info = this.getArrowInfo(shape)
const bounds = this.bounds(shape) const bounds = this.editor.getBounds(shape)
const { text, font, size } = shape.props const { text, font, size } = shape.props
if (!info) return null if (!info) return null
@ -901,10 +901,6 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
return this.labelBoundsCache.get(shape.id) || null return this.labelBoundsCache.get(shape.id) || null
} }
getEditingBounds = (shape: TLArrowShape): Box2d => {
return this.getLabelBounds(shape) ?? new Box2d()
}
onEditEnd: TLOnEditEndHandler<TLArrowShape> = (shape) => { onEditEnd: TLOnEditEndHandler<TLArrowShape> = (shape) => {
const { const {
id, id,
@ -941,7 +937,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
// Arrowhead end path // Arrowhead end path
const ae = info.end.arrowhead && getArrowheadPathForType(info, 'end', strokeWidth) const ae = info.end.arrowhead && getArrowheadPathForType(info, 'end', strokeWidth)
const bounds = this.bounds(shape) const bounds = this.editor.getBounds(shape)
const labelSize = this.getLabelBounds(shape) const labelSize = this.getLabelBounds(shape)
const maskId = (shape.id + '_clip').replace(':', '_') const maskId = (shape.id + '_clip').replace(':', '_')

View file

@ -74,7 +74,7 @@ export function getCurvedArrowInfo(editor: Editor, shape: TLArrowShape, extraBen
const endInStartShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, endInPageSpace) const endInStartShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, endInPageSpace)
const centerInStartShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, centerInPageSpace) const centerInStartShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, centerInPageSpace)
const isClosed = startShapeInfo.util.isClosed(startShapeInfo.shape) const { isClosed } = startShapeInfo
const fn = isClosed ? intersectCirclePolygon : intersectCirclePolyline const fn = isClosed ? intersectCirclePolygon : intersectCirclePolyline
let point: VecLike | undefined let point: VecLike | undefined
@ -82,7 +82,7 @@ export function getCurvedArrowInfo(editor: Editor, shape: TLArrowShape, extraBen
let intersections = fn( let intersections = fn(
centerInStartShapeLocalSpace, centerInStartShapeLocalSpace,
handleArc.radius, handleArc.radius,
startShapeInfo.util.outline(startShapeInfo.shape) editor.getOutline(startShapeInfo.shape)
) )
if (intersections) { if (intersections) {
@ -150,7 +150,7 @@ export function getCurvedArrowInfo(editor: Editor, shape: TLArrowShape, extraBen
const endInEndShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, endInPageSpace) const endInEndShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, endInPageSpace)
const centerInEndShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, centerInPageSpace) const centerInEndShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, centerInPageSpace)
const isClosed = endShapeInfo.util.isClosed(endShapeInfo.shape) const isClosed = endShapeInfo.isClosed
const fn = isClosed ? intersectCirclePolygon : intersectCirclePolyline const fn = isClosed ? intersectCirclePolygon : intersectCirclePolyline
const angleToMiddle = Vec2d.Angle(handleArc.center, middle) const angleToMiddle = Vec2d.Angle(handleArc.center, middle)
@ -162,7 +162,7 @@ export function getCurvedArrowInfo(editor: Editor, shape: TLArrowShape, extraBen
let intersections = fn( let intersections = fn(
centerInEndShapeLocalSpace, centerInEndShapeLocalSpace,
handleArc.radius, handleArc.radius,
endShapeInfo.util.outline(endShapeInfo.shape) editor.getOutline(endShapeInfo.shape)
) )
if (intersections) { if (intersections) {

View file

@ -1,7 +1,6 @@
import { Matrix2d, Vec2d } from '@tldraw/primitives' import { Matrix2d, Vec2d } from '@tldraw/primitives'
import { TLArrowShape, TLArrowShapeTerminal, TLShape } from '@tldraw/tlschema' import { TLArrowShape, TLArrowShapeTerminal, TLShape } from '@tldraw/tlschema'
import { Editor } from '../../../Editor' import { Editor } from '../../../Editor'
import { ShapeUtil } from '../../ShapeUtil'
export function getIsArrowStraight(shape: TLArrowShape) { export function getIsArrowStraight(shape: TLArrowShape) {
return Math.abs(shape.props.bend) < 8 // snap to +-8px return Math.abs(shape.props.bend) < 8 // snap to +-8px
@ -9,10 +8,11 @@ export function getIsArrowStraight(shape: TLArrowShape) {
export type BoundShapeInfo<T extends TLShape = TLShape> = { export type BoundShapeInfo<T extends TLShape = TLShape> = {
shape: T shape: T
util: ShapeUtil<T>
didIntersect: boolean didIntersect: boolean
isExact: boolean isExact: boolean
isClosed: boolean
transform: Matrix2d transform: Matrix2d
outline: Vec2d[]
} }
export function getBoundShapeInfoForTerminal( export function getBoundShapeInfoForTerminal(
@ -29,10 +29,11 @@ export function getBoundShapeInfoForTerminal(
return { return {
shape, shape,
util,
transform, transform,
isClosed: util.isClosed(shape),
isExact: terminal.isExact, isExact: terminal.isExact,
didIntersect: false, didIntersect: false,
outline: editor.getOutline(shape),
} }
} }

View file

@ -67,14 +67,14 @@ export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): Arrow
if (endShapeInfo.didIntersect && !startShapeInfo.didIntersect) { if (endShapeInfo.didIntersect && !startShapeInfo.didIntersect) {
// ...and if only the end shape intersected, then make it // ...and if only the end shape intersected, then make it
// a short arrow ending at the end shape intersection. // a short arrow ending at the end shape intersection.
if (startShapeInfo.util.isClosed(startShapeInfo.shape)) { if (startShapeInfo.isClosed) {
a.setTo(Vec2d.Nudge(b, a, minDist)) a.setTo(Vec2d.Nudge(b, a, minDist))
} }
} else if (!endShapeInfo.didIntersect) { } else if (!endShapeInfo.didIntersect) {
// ...and if only the end shape intersected, or if neither // ...and if only the end shape intersected, or if neither
// shape intersected, then make it a short arrow starting // shape intersected, then make it a short arrow starting
// at the start shape intersection. // at the start shape intersection.
if (endShapeInfo.util.isClosed(endShapeInfo.shape)) { if (endShapeInfo.isClosed) {
b.setTo(Vec2d.Nudge(a, b, minDist)) b.setTo(Vec2d.Nudge(a, b, minDist))
} }
} }
@ -179,10 +179,10 @@ function updateArrowheadPointWithBoundShape(
const targetFrom = Matrix2d.applyToPoint(Matrix2d.Inverse(targetShapeInfo.transform), pageFrom) const targetFrom = Matrix2d.applyToPoint(Matrix2d.Inverse(targetShapeInfo.transform), pageFrom)
const targetTo = Matrix2d.applyToPoint(Matrix2d.Inverse(targetShapeInfo.transform), pageTo) const targetTo = Matrix2d.applyToPoint(Matrix2d.Inverse(targetShapeInfo.transform), pageTo)
const isClosed = targetShapeInfo.util.isClosed(targetShapeInfo.shape) const isClosed = targetShapeInfo.isClosed
const fn = isClosed ? intersectLineSegmentPolygon : intersectLineSegmentPolyline const fn = isClosed ? intersectLineSegmentPolygon : intersectLineSegmentPolyline
const intersection = fn(targetFrom, targetTo, targetShapeInfo.util.outline(targetShapeInfo.shape)) const intersection = fn(targetFrom, targetTo, targetShapeInfo.outline)
let targetInt: VecLike | undefined let targetInt: VecLike | undefined

View file

@ -47,7 +47,7 @@ export class Pointing extends StateNode {
const shape = this.editor.getShapeById<TLArrowShape>(id) const shape = this.editor.getShapeById<TLArrowShape>(id)
if (!shape) return if (!shape) return
const handles = util.handles?.(shape) const handles = this.editor.getHandles(shape)
if (handles) { if (handles) {
// start precise // start precise
@ -82,8 +82,7 @@ export class Pointing extends StateNode {
if (!this.shape) return if (!this.shape) return
if (this.editor.inputs.isDragging) { if (this.editor.inputs.isDragging) {
const util = this.editor.getShapeUtil(this.shape) const handles = this.editor.getHandles(this.shape)
const handles = util.handles?.(this.shape)
if (!handles) { if (!handles) {
this.editor.bailToMark('creating') this.editor.bailToMark('creating')
@ -96,8 +95,6 @@ export class Pointing extends StateNode {
if (!shape) return if (!shape) return
const handles = util.handles(shape)
if (handles) { if (handles) {
const { x, y } = this.editor.getPointInShapeSpace( const { x, y } = this.editor.getPointInShapeSpace(
shape, shape,

View file

@ -23,7 +23,7 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
override hideSelectionBoundsBg = () => true override hideSelectionBoundsBg = () => true
override hideSelectionBoundsFg = () => true override hideSelectionBoundsFg = () => true
override defaultProps(): TLBookmarkShape['props'] { override getDefaultProps(): TLBookmarkShape['props'] {
return { return {
url: '', url: '',
w: 300, w: 300,

View file

@ -30,7 +30,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
hideSelectionBoundsBg = (shape: TLDrawShape) => getIsDot(shape) hideSelectionBoundsBg = (shape: TLDrawShape) => getIsDot(shape)
hideSelectionBoundsFg = (shape: TLDrawShape) => getIsDot(shape) hideSelectionBoundsFg = (shape: TLDrawShape) => getIsDot(shape)
override defaultProps(): TLDrawShape['props'] { override getDefaultProps(): TLDrawShape['props'] {
return { return {
segments: [], segments: [],
color: 'black', color: 'black',
@ -46,7 +46,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
isClosed = (shape: TLDrawShape) => shape.props.isClosed isClosed = (shape: TLDrawShape) => shape.props.isClosed
getBounds(shape: TLDrawShape) { getBounds(shape: TLDrawShape) {
return Box2d.FromPoints(this.outline(shape)) return Box2d.FromPoints(this.editor.getOutline(shape))
} }
getOutline(shape: TLDrawShape) { getOutline(shape: TLDrawShape) {
@ -54,11 +54,11 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
} }
getCenter(shape: TLDrawShape): Vec2d { getCenter(shape: TLDrawShape): Vec2d {
return this.bounds(shape).center return this.editor.getBounds(shape).center
} }
hitTestPoint(shape: TLDrawShape, point: VecLike): boolean { hitTestPoint(shape: TLDrawShape, point: VecLike): boolean {
const outline = this.outline(shape) const outline = this.editor.getOutline(shape)
const zoomLevel = this.editor.zoomLevel const zoomLevel = this.editor.zoomLevel
const offsetDist = STROKE_SIZES[shape.props.size] / zoomLevel const offsetDist = STROKE_SIZES[shape.props.size] / zoomLevel
@ -72,7 +72,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
return pointInPolygon(point, outline) return pointInPolygon(point, outline)
} }
if (this.bounds(shape).containsPoint(point)) { if (this.editor.getBounds(shape).containsPoint(point)) {
for (let i = 0; i < outline.length; i++) { for (let i = 0; i < outline.length; i++) {
const C = outline[i] const C = outline[i]
const D = outline[(i + 1) % outline.length] const D = outline[(i + 1) % outline.length]
@ -85,7 +85,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
} }
hitTestLineSegment(shape: TLDrawShape, A: VecLike, B: VecLike): boolean { hitTestLineSegment(shape: TLDrawShape, A: VecLike, B: VecLike): boolean {
const outline = this.outline(shape) const outline = this.editor.getOutline(shape)
if (shape.props.segments.length === 1 && shape.props.segments[0].points.length < 4) { if (shape.props.segments.length === 1 && shape.props.segments[0].points.length < 4) {
const zoomLevel = this.editor.zoomLevel const zoomLevel = this.editor.zoomLevel

View file

@ -38,7 +38,7 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
return !!getEmbedInfo(shape.props.url)?.definition?.doesResize return !!getEmbedInfo(shape.props.url)?.definition?.doesResize
} }
override defaultProps(): TLEmbedShape['props'] { override getDefaultProps(): TLEmbedShape['props'] {
return { return {
w: 300, w: 300,
h: 300, h: 300,

View file

@ -18,12 +18,12 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
override canEdit = () => true override canEdit = () => true
override defaultProps(): TLFrameShape['props'] { override getDefaultProps(): TLFrameShape['props'] {
return { w: 160 * 2, h: 90 * 2, name: '' } return { w: 160 * 2, h: 90 * 2, name: '' }
} }
override component(shape: TLFrameShape) { override component(shape: TLFrameShape) {
const bounds = this.bounds(shape) const bounds = this.editor.getBounds(shape)
return ( return (
<> <>
@ -137,7 +137,7 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
} }
indicator(shape: TLFrameShape) { indicator(shape: TLFrameShape) {
const bounds = this.bounds(shape) const bounds = this.editor.getBounds(shape)
return ( return (
<rect <rect

View file

@ -50,7 +50,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
canEdit = () => true canEdit = () => true
override defaultProps(): TLGeoShape['props'] { override getDefaultProps(): TLGeoShape['props'] {
return { return {
w: 100, w: 100,
h: 100, h: 100,
@ -70,7 +70,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
} }
hitTestLineSegment(shape: TLGeoShape, A: VecLike, B: VecLike): boolean { hitTestLineSegment(shape: TLGeoShape, A: VecLike, B: VecLike): boolean {
const outline = this.outline(shape) const outline = this.editor.getOutline(shape)
// Check the outline // Check the outline
for (let i = 0; i < outline.length; i++) { for (let i = 0; i < outline.length; i++) {
@ -91,7 +91,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
} }
hitTestPoint(shape: TLGeoShape, point: VecLike): boolean { hitTestPoint(shape: TLGeoShape, point: VecLike): boolean {
const outline = this.outline(shape) const outline = this.editor.getOutline(shape)
if (shape.props.fill === 'none') { if (shape.props.fill === 'none') {
const zoomLevel = this.editor.zoomLevel const zoomLevel = this.editor.zoomLevel
@ -397,7 +397,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
break break
} }
default: { default: {
const outline = this.outline(shape) const outline = this.editor.getOutline(shape)
const lines = getLines(shape.props, strokeWidth) const lines = getLines(shape.props, strokeWidth)
if (dash === 'solid' || (dash === 'draw' && forceSolid)) { if (dash === 'solid' || (dash === 'draw' && forceSolid)) {
@ -479,7 +479,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
} }
default: { default: {
const outline = this.outline(shape) const outline = this.editor.getOutline(shape)
let path: string let path: string
if (props.dash === 'draw' && !forceSolid) { if (props.dash === 'draw' && !forceSolid) {
@ -591,7 +591,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
break break
} }
default: { default: {
const outline = this.outline(shape) const outline = this.editor.getOutline(shape)
const lines = getLines(shape.props, strokeWidth) const lines = getLines(shape.props, strokeWidth)
switch (props.dash) { switch (props.dash) {
@ -635,7 +635,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
} }
if (props.text) { if (props.text) {
const bounds = this.bounds(shape) const bounds = this.editor.getBounds(shape)
const rootTextElm = getTextLabelSvgElement({ const rootTextElm = getTextLabelSvgElement({
editor: this.editor, editor: this.editor,

View file

@ -15,7 +15,7 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
canBind = () => false canBind = () => false
defaultProps(): TLGroupShape['props'] { getDefaultProps(): TLGroupShape['props'] {
return {} return {}
} }
@ -36,11 +36,11 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
} }
getCenter(shape: TLGroupShape): Vec2d { getCenter(shape: TLGroupShape): Vec2d {
return this.bounds(shape).center return this.editor.getBounds(shape).center
} }
getOutline(shape: TLGroupShape): Vec2d[] { getOutline(shape: TLGroupShape): Vec2d[] {
return this.bounds(shape).corners return this.editor.getBounds(shape).corners
} }
component(shape: TLGroupShape) { component(shape: TLGroupShape) {
@ -71,7 +71,7 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
return null return null
} }
const bounds = this.bounds(shape) const bounds = this.editor.getBounds(shape)
return ( return (
<SVGContainer id={shape.id}> <SVGContainer id={shape.id}>
@ -86,7 +86,7 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
camera: { z: zoomLevel }, camera: { z: zoomLevel },
} = this.editor } = this.editor
const bounds = this.bounds(shape) const bounds = this.editor.getBounds(shape)
return <DashedOutlineBox className="" bounds={bounds} zoomLevel={zoomLevel} /> return <DashedOutlineBox className="" bounds={bounds} zoomLevel={zoomLevel} />
} }

View file

@ -22,7 +22,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
hideSelectionBoundsBg = (shape: TLHighlightShape) => getIsDot(shape) hideSelectionBoundsBg = (shape: TLHighlightShape) => getIsDot(shape)
hideSelectionBoundsFg = (shape: TLHighlightShape) => getIsDot(shape) hideSelectionBoundsFg = (shape: TLHighlightShape) => getIsDot(shape)
override defaultProps(): TLHighlightShape['props'] { override getDefaultProps(): TLHighlightShape['props'] {
return { return {
segments: [], segments: [],
color: 'black', color: 'black',
@ -33,7 +33,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
} }
getBounds(shape: TLHighlightShape) { getBounds(shape: TLHighlightShape) {
return Box2d.FromPoints(this.outline(shape)) return Box2d.FromPoints(this.editor.getOutline(shape))
} }
getOutline(shape: TLHighlightShape) { getOutline(shape: TLHighlightShape) {
@ -41,11 +41,11 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
} }
getCenter(shape: TLHighlightShape): Vec2d { getCenter(shape: TLHighlightShape): Vec2d {
return this.bounds(shape).center return this.editor.getBounds(shape).center
} }
hitTestPoint(shape: TLHighlightShape, point: VecLike): boolean { hitTestPoint(shape: TLHighlightShape, point: VecLike): boolean {
const outline = this.outline(shape) const outline = this.editor.getOutline(shape)
const zoomLevel = this.editor.zoomLevel const zoomLevel = this.editor.zoomLevel
const offsetDist = getStrokeWidth(shape) / zoomLevel const offsetDist = getStrokeWidth(shape) / zoomLevel
@ -55,7 +55,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
} }
} }
if (this.bounds(shape).containsPoint(point)) { if (this.editor.getBounds(shape).containsPoint(point)) {
for (let i = 0; i < outline.length; i++) { for (let i = 0; i < outline.length; i++) {
const C = outline[i] const C = outline[i]
const D = outline[(i + 1) % outline.length] const D = outline[(i + 1) % outline.length]
@ -68,7 +68,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
} }
hitTestLineSegment(shape: TLHighlightShape, A: VecLike, B: VecLike): boolean { hitTestLineSegment(shape: TLHighlightShape, A: VecLike, B: VecLike): boolean {
const outline = this.outline(shape) const outline = this.editor.getOutline(shape)
if (shape.props.segments.length === 1 && shape.props.segments[0].points.length < 4) { if (shape.props.segments.length === 1 && shape.props.segments[0].points.length < 4) {
const zoomLevel = this.editor.zoomLevel const zoomLevel = this.editor.zoomLevel
@ -102,7 +102,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
) )
} }
renderBackground(shape: TLHighlightShape) { backgroundComponent(shape: TLHighlightShape) {
return ( return (
<HighlightRenderer <HighlightRenderer
strokeWidth={getStrokeWidth(shape)} strokeWidth={getStrokeWidth(shape)}

View file

@ -54,7 +54,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
override isAspectRatioLocked = () => true override isAspectRatioLocked = () => true
override canCrop = () => true override canCrop = () => true
override defaultProps(): TLImageShape['props'] { override getDefaultProps(): TLImageShape['props'] {
return { return {
w: 100, w: 100,
h: 100, h: 100,

View file

@ -35,7 +35,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
override hideSelectionBoundsFg = () => true override hideSelectionBoundsFg = () => true
override isClosed = () => false override isClosed = () => false
override defaultProps(): TLLineShape['props'] { override getDefaultProps(): TLLineShape['props'] {
return { return {
dash: 'draw', dash: 'draw',
size: 'm', size: 'm',
@ -68,10 +68,6 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
return spline.bounds return spline.bounds
} }
getCenter(shape: TLLineShape) {
return this.bounds(shape).center
}
getHandles(shape: TLLineShape) { getHandles(shape: TLLineShape) {
return handlesCache.get(shape.props, () => { return handlesCache.get(shape.props, () => {
const handles = shape.props.handles const handles = shape.props.handles
@ -174,11 +170,11 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
hitTestPoint(shape: TLLineShape, point: Vec2d): boolean { hitTestPoint(shape: TLLineShape, point: Vec2d): boolean {
const zoomLevel = this.editor.zoomLevel const zoomLevel = this.editor.zoomLevel
const offsetDist = STROKE_SIZES[shape.props.size] / zoomLevel const offsetDist = STROKE_SIZES[shape.props.size] / zoomLevel
return pointNearToPolyline(point, this.outline(shape), offsetDist) return pointNearToPolyline(point, this.editor.getOutline(shape), offsetDist)
} }
hitTestLineSegment(shape: TLLineShape, A: VecLike, B: VecLike): boolean { hitTestLineSegment(shape: TLLineShape, A: VecLike, B: VecLike): boolean {
return intersectLineSegmentPolyline(A, B, this.outline(shape)) !== null return intersectLineSegmentPolyline(A, B, this.editor.getOutline(shape)) !== null
} }
component(shape: TLLineShape) { component(shape: TLLineShape) {

View file

@ -29,7 +29,8 @@ export class Pointing extends StateNode {
// if user is holding shift then we are adding points to an existing line // if user is holding shift then we are adding points to an existing line
if (inputs.shiftKey && shapeExists) { if (inputs.shiftKey && shapeExists) {
const handles = this.editor.getShapeUtil(this.shape).handles(this.shape) const handles = this.editor.getHandles(this.shape)
if (!handles) return
const vertexHandles = handles.filter((h) => h.type === 'vertex').sort(sortByIndex) const vertexHandles = handles.filter((h) => h.type === 'vertex').sort(sortByIndex)
const endHandle = vertexHandles[vertexHandles.length - 1] const endHandle = vertexHandles[vertexHandles.length - 1]
@ -96,8 +97,7 @@ export class Pointing extends StateNode {
if (!this.shape) return if (!this.shape) return
if (this.editor.inputs.isDragging) { if (this.editor.inputs.isDragging) {
const util = this.editor.getShapeUtil(this.shape) const handles = this.editor.getHandles(this.shape)
const handles = util.handles?.(this.shape)
if (!handles) { if (!handles) {
this.editor.bailToMark('creating') this.editor.bailToMark('creating')
throw Error('No handles found') throw Error('No handles found')

View file

@ -19,7 +19,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
hideSelectionBoundsBg = () => true hideSelectionBoundsBg = () => true
hideSelectionBoundsFg = () => true hideSelectionBoundsFg = () => true
defaultProps(): TLNoteShape['props'] { getDefaultProps(): TLNoteShape['props'] {
return { return {
color: 'black', color: 'black',
size: 'm', size: 'm',
@ -42,7 +42,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
} }
getOutline(shape: TLNoteShape) { getOutline(shape: TLNoteShape) {
return this.bounds(shape).corners return this.editor.getBounds(shape).corners
} }
getCenter(_shape: TLNoteShape) { getCenter(_shape: TLNoteShape) {
@ -106,7 +106,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
} }
toSvg(shape: TLNoteShape, font: string, colors: TLExportColors) { toSvg(shape: TLNoteShape, font: string, colors: TLExportColors) {
const bounds = this.bounds(shape) const bounds = this.getBounds(shape)
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g') const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')

View file

@ -1,5 +1,4 @@
import { TLNoteShape, createShapeId } from '@tldraw/tlschema' import { TLNoteShape, createShapeId } from '@tldraw/tlschema'
import { NoteShapeUtil } from '../../../shapes/note/NoteShapeUtil'
import { StateNode } from '../../../tools/StateNode' import { StateNode } from '../../../tools/StateNode'
import { TLEventHandlers, TLInterruptEvent, TLPointerEventInfo } from '../../../types/event-types' import { TLEventHandlers, TLInterruptEvent, TLPointerEventInfo } from '../../../types/event-types'
@ -97,9 +96,8 @@ export class Pointing extends StateNode {
true true
) )
const util = this.editor.getShapeUtil(NoteShapeUtil)
const shape = this.editor.getShapeById<TLNoteShape>(id)! const shape = this.editor.getShapeById<TLNoteShape>(id)!
const bounds = util.bounds(shape) const bounds = this.editor.getBounds(shape)
// Center the text around the created point // Center the text around the created point
this.editor.updateShapes([ this.editor.updateShapes([

View file

@ -24,7 +24,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
isAspectRatioLocked: TLShapeUtilFlag<TLTextShape> = () => true isAspectRatioLocked: TLShapeUtilFlag<TLTextShape> = () => true
defaultProps(): TLTextShape['props'] { getDefaultProps(): TLTextShape['props'] {
return { return {
color: 'black', color: 'black',
size: 'm', size: 'm',
@ -63,7 +63,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
} }
getOutline(shape: TLTextShape) { getOutline(shape: TLTextShape) {
const bounds = this.bounds(shape) const bounds = this.editor.getBounds(shape)
return [ return [
new Vec2d(0, 0), new Vec2d(0, 0),
@ -73,11 +73,6 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
] ]
} }
getCenter(shape: TLTextShape): Vec2d {
const bounds = this.bounds(shape)
return new Vec2d(bounds.width / 2, bounds.height / 2)
}
component(shape: TLTextShape) { component(shape: TLTextShape) {
const { const {
id, id,
@ -150,12 +145,12 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
} }
indicator(shape: TLTextShape) { indicator(shape: TLTextShape) {
const bounds = this.bounds(shape) const bounds = this.getBounds(shape)
return <rect width={toDomPrecision(bounds.width)} height={toDomPrecision(bounds.height)} /> return <rect width={toDomPrecision(bounds.width)} height={toDomPrecision(bounds.height)} />
} }
toSvg(shape: TLTextShape, font: string | undefined, colors: TLExportColors) { toSvg(shape: TLTextShape, font: string | undefined, colors: TLExportColors) {
const bounds = this.bounds(shape) const bounds = this.getBounds(shape)
const text = shape.props.text const text = shape.props.text
const width = bounds.width / (shape.props.scale ?? 1) const width = bounds.width / (shape.props.scale ?? 1)
@ -204,7 +199,11 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
const { initialBounds, initialShape, scaleX, handle } = info const { initialBounds, initialShape, scaleX, handle } = info
if (info.mode === 'scale_shape' || (handle !== 'right' && handle !== 'left')) { if (info.mode === 'scale_shape' || (handle !== 'right' && handle !== 'left')) {
return resizeScaled(shape, info) return {
id: shape.id,
type: shape.type,
...resizeScaled(shape, info),
}
} else { } else {
const prevWidth = initialBounds.width const prevWidth = initialBounds.width
let nextWidth = prevWidth * scaleX let nextWidth = prevWidth * scaleX
@ -227,6 +226,8 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
const { x, y } = offset.rot(shape.rotation).add(initialShape) const { x, y } = offset.rot(shape.rotation).add(initialShape)
return { return {
id: shape.id,
type: shape.type,
x, x,
y, y,
props: { props: {

View file

@ -16,7 +16,7 @@ export class VideoShapeUtil extends BaseBoxShapeUtil<TLVideoShape> {
override canEdit = () => true override canEdit = () => true
override isAspectRatioLocked = () => true override isAspectRatioLocked = () => true
override defaultProps(): TLVideoShape['props'] { override getDefaultProps(): TLVideoShape['props'] {
return { return {
w: 100, w: 100,
h: 100, h: 100,

View file

@ -94,7 +94,7 @@ export class Pointing extends StateNode {
]) ])
const shape = this.editor.getShapeById<TLBaseBoxShape>(id)! const shape = this.editor.getShapeById<TLBaseBoxShape>(id)!
const { w, h } = this.editor.getShapeUtil(shape).defaultProps() as TLBaseBoxShape['props'] const { w, h } = this.editor.getShapeUtil(shape).getDefaultProps() as TLBaseBoxShape['props']
const delta = this.editor.getDeltaInParentSpace(shape, new Vec2d(w / 2, h / 2)) const delta = this.editor.getDeltaInParentSpace(shape, new Vec2d(w / 2, h / 2))
this.editor.updateShapes<TLBaseBoxShape>([ this.editor.updateShapes<TLBaseBoxShape>([

View file

@ -5,6 +5,7 @@ let editor: TestEditor
const ids = { const ids = {
box1: createShapeId('box1'), box1: createShapeId('box1'),
line1: createShapeId('line1'),
embed1: createShapeId('embed1'), embed1: createShapeId('embed1'),
} }
@ -135,7 +136,8 @@ describe('PointingHandle', () => {
describe('DraggingHandle', () => { describe('DraggingHandle', () => {
it('Enters from pointing_handle and exits to idle', () => { it('Enters from pointing_handle and exits to idle', () => {
const shape = editor.getShapeById(ids.box1) editor.createShapes([{ id: ids.line1, type: 'line', x: 100, y: 100 }])
const shape = editor.getShapeById(ids.line1)
editor.pointerDown(150, 150, { editor.pointerDown(150, 150, {
target: 'handle', target: 'handle',
shape, shape,
@ -149,7 +151,8 @@ describe('DraggingHandle', () => {
}) })
it('Bails on escape', () => { it('Bails on escape', () => {
const shape = editor.getShapeById(ids.box1) editor.createShapes([{ id: ids.line1, type: 'line', x: 100, y: 100 }])
const shape = editor.getShapeById(ids.line1)
editor.pointerDown(150, 150, { editor.pointerDown(150, 150, {
target: 'handle', target: 'handle',

View file

@ -58,7 +58,7 @@ export class DraggingHandle extends StateNode {
this.editor.setCursor({ type: isCreating ? 'cross' : 'grabbing', rotation: 0 }) this.editor.setCursor({ type: isCreating ? 'cross' : 'grabbing', rotation: 0 })
// <!-- Only relevant to arrows // <!-- Only relevant to arrows
const handles = this.editor.getShapeUtil(shape).handles(shape).sort(sortByIndex) const handles = this.editor.getHandles(shape)!.sort(sortByIndex)
const index = handles.findIndex((h) => h.id === info.handle.id) const index = handles.findIndex((h) => h.id === info.handle.id)
// Find the adjacent handle // Find the adjacent handle
@ -227,14 +227,14 @@ export class DraggingHandle extends StateNode {
// Get all the outline segments from the shape // Get all the outline segments from the shape
const additionalSegments = util const additionalSegments = util
.outlineSegments(shape) .getOutlineSegments(shape)
.map((segment) => Matrix2d.applyToPoints(pageTransform, segment)) .map((segment) => Matrix2d.applyToPoints(pageTransform, segment))
// We want to skip the segments that include the handle, so // We want to skip the segments that include the handle, so
// find the index of the handle that shares the same index property // find the index of the handle that shares the same index property
// as the initial dragging handle; this catches a quirk of create handles // as the initial dragging handle; this catches a quirk of create handles
const handleIndex = util const handleIndex = editor
.handles(shape) .getHandles(shape)!
.filter(({ type }) => type === 'vertex') .filter(({ type }) => type === 'vertex')
.sort(sortByIndex) .sort(sortByIndex)
.findIndex(({ index }) => initialHandle.index === index) .findIndex(({ index }) => initialHandle.index === index)

View file

@ -405,7 +405,7 @@ export class Resizing extends StateNode {
return { return {
shape, shape,
bounds: util.bounds(shape), bounds: this.editor.getBounds(shape),
pageTransform, pageTransform,
pageRotation: Matrix2d.Decompose(pageTransform!).rotation, pageRotation: Matrix2d.Decompose(pageTransform!).rotation,
isAspectRatioLocked: util.isAspectRatioLocked(shape), isAspectRatioLocked: util.isAspectRatioLocked(shape),

View file

@ -11,6 +11,21 @@ export function useDocumentEvents() {
const isAppFocused = useValue('isFocused', () => editor.isFocused, [editor]) const isAppFocused = useValue('isFocused', () => editor.isFocused, [editor])
useEffect(() => {
if (typeof matchMedia !== undefined) return
function updateDevicePixelRatio() {
editor.setDevicePixelRatio(window.devicePixelRatio)
}
const MM = matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
MM.addEventListener('change', updateDevicePixelRatio)
return () => {
MM.removeEventListener('change', updateDevicePixelRatio)
}
}, [editor])
useEffect(() => { useEffect(() => {
if (!isAppFocused) return if (!isAppFocused) return

View file

@ -7,8 +7,7 @@ import { useEditor } from './useEditor'
function getHandle(editor: Editor, id: TLShapeId, handleId: string) { function getHandle(editor: Editor, id: TLShapeId, handleId: string) {
const shape = editor.getShapeById<TLArrowShape | TLLineShape>(id)! const shape = editor.getShapeById<TLArrowShape | TLLineShape>(id)!
const util = editor.getShapeUtil(shape) const handles = editor.getHandles(shape)!
const handles = util.handles(shape)
return { shape, handle: handles.find((h) => h.id === handleId) } return { shape, handle: handles.find((h) => h.id === handleId) }
} }

View file

@ -455,7 +455,7 @@ describe('getShapeUtil', () => {
class MyFakeShapeUtil extends BaseBoxShapeUtil<any> { class MyFakeShapeUtil extends BaseBoxShapeUtil<any> {
static type = 'fake' static type = 'fake'
defaultProps() { getDefaultProps() {
throw new Error('Method not implemented.') throw new Error('Method not implemented.')
} }
component() { component() {
@ -475,7 +475,7 @@ describe('getShapeUtil', () => {
class MyFakeGeoShapeUtil extends BaseBoxShapeUtil<any> { class MyFakeGeoShapeUtil extends BaseBoxShapeUtil<any> {
static type = 'geo' static type = 'geo'
defaultProps() { getDefaultProps() {
throw new Error('Method not implemented.') throw new Error('Method not implemented.')
} }
component() { component() {

View file

@ -275,7 +275,7 @@ describe('Custom shapes', () => {
override canResize = (_shape: CardShape) => true override canResize = (_shape: CardShape) => true
override canBind = (_shape: CardShape) => true override canBind = (_shape: CardShape) => true
override defaultProps(): CardShape['props'] { override getDefaultProps(): CardShape['props'] {
return { return {
w: 300, w: 300,
h: 300, h: 300,
@ -283,7 +283,7 @@ describe('Custom shapes', () => {
} }
component(shape: CardShape) { component(shape: CardShape) {
const bounds = this.bounds(shape) const bounds = this.getBounds(shape)
return ( return (
<HTMLContainer <HTMLContainer

View file

@ -33,7 +33,7 @@ describe('creating frames', () => {
editor.setSelectedTool('frame') editor.setSelectedTool('frame')
editor.pointerDown(100, 100).pointerUp(100, 100) editor.pointerDown(100, 100).pointerUp(100, 100)
expect(editor.onlySelectedShape?.type).toBe('frame') expect(editor.onlySelectedShape?.type).toBe('frame')
const { w, h } = editor.getShapeUtil(FrameShapeUtil).defaultProps() const { w, h } = editor.getShapeUtil(FrameShapeUtil).getDefaultProps()
expect(editor.getPageBounds(editor.onlySelectedShape!)).toMatchObject({ expect(editor.getPageBounds(editor.onlySelectedShape!)).toMatchObject({
x: 100 - w / 2, x: 100 - w / 2,
y: 100 - h / 2, y: 100 - h / 2,

View file

@ -11,7 +11,6 @@ import {
} from '@tldraw/tlschema' } from '@tldraw/tlschema'
import { assert, compact } from '@tldraw/utils' import { assert, compact } from '@tldraw/utils'
import { ArrowShapeTool } from '../../editor/shapes/arrow/ArrowShapeTool' import { ArrowShapeTool } from '../../editor/shapes/arrow/ArrowShapeTool'
import { ArrowShapeUtil } from '../../editor/shapes/arrow/ArrowShapeUtil'
import { DrawShapeTool } from '../../editor/shapes/draw/DrawShapeTool' import { DrawShapeTool } from '../../editor/shapes/draw/DrawShapeTool'
import { GroupShapeUtil } from '../../editor/shapes/group/GroupShapeUtil' import { GroupShapeUtil } from '../../editor/shapes/group/GroupShapeUtil'
import { LineShapeTool } from '../../editor/shapes/line/LineShapeTool' import { LineShapeTool } from '../../editor/shapes/line/LineShapeTool'
@ -1679,10 +1678,7 @@ describe('moving handles within a group', () => {
editor.pointerDown(60, 60, { editor.pointerDown(60, 60, {
target: 'handle', target: 'handle',
shape: arrow, shape: arrow,
handle: editor handle: editor.getHandles<TLArrowShape>(arrow)!.find((h) => h.id === 'end'),
.getShapeUtil(ArrowShapeUtil)
.handles(arrow)
.find((h) => h.id === 'end'),
}) })
editor.expectToBeIn('select.pointing_handle') editor.expectToBeIn('select.pointing_handle')

View file

@ -14,7 +14,7 @@ type __TopLeftSnapOnlyShape = any
class __TopLeftSnapOnlyShapeUtil extends ShapeUtil<__TopLeftSnapOnlyShape> { class __TopLeftSnapOnlyShapeUtil extends ShapeUtil<__TopLeftSnapOnlyShape> {
static override type = '__test_top_left_snap_only' as const static override type = '__test_top_left_snap_only' as const
defaultProps(): __TopLeftSnapOnlyShape['props'] { getDefaultProps(): __TopLeftSnapOnlyShape['props'] {
return { width: 10, height: 10 } return { width: 10, height: 10 }
} }
getBounds(shape: __TopLeftSnapOnlyShape): Box2d { getBounds(shape: __TopLeftSnapOnlyShape): Box2d {

View file

@ -550,7 +550,7 @@ export function buildFromV1Document(editor: Editor, document: LegacyTldrawDocume
y: bounds.minY + bounds.height * ny, y: bounds.minY + bounds.height * ny,
}) })
const handles = util.handles(v2ShapeFresh) const handles = editor.getHandles(v2ShapeFresh)!
const change = util.onHandleChange!(v2ShapeFresh, { const change = util.onHandleChange!(v2ShapeFresh, {
handle: { handle: {

View file

@ -12,6 +12,7 @@ export function Tldraw(props: TldrawEditorProps & TldrawUiProps): JSX.Element;
export * from "@tldraw/editor"; export * from "@tldraw/editor";
export * from "@tldraw/primitives";
export * from "@tldraw/ui"; export * from "@tldraw/ui";
// (No @packageDocumentation comment for this package) // (No @packageDocumentation comment for this package)

View file

@ -46,6 +46,8 @@
"dependencies": { "dependencies": {
"@tldraw/editor": "workspace:*", "@tldraw/editor": "workspace:*",
"@tldraw/polyfills": "workspace:*", "@tldraw/polyfills": "workspace:*",
"@tldraw/primitives": "workspace:*",
"@tldraw/store": "workspace:*",
"@tldraw/ui": "workspace:*" "@tldraw/ui": "workspace:*"
}, },
"peerDependencies": { "peerDependencies": {

View file

@ -2,9 +2,10 @@
/** @internal */ /** @internal */
import '@tldraw/polyfills' import '@tldraw/polyfills'
// eslint-disable-next-line local/no-export-star // eslint-disable-next-line local/no-export-star
export * from '@tldraw/editor' export * from '@tldraw/editor'
// eslint-disable-next-line local/no-export-star // eslint-disable-next-line local/no-export-star
export * from '@tldraw/primitives'
// eslint-disable-next-line local/no-export-star
export * from '@tldraw/ui' export * from '@tldraw/ui'
export { Tldraw } from './lib/Tldraw' export { Tldraw } from './lib/Tldraw'

View file

@ -706,7 +706,7 @@ export type ShapeProps<Shape extends TLBaseShape<any, any>> = {
// @public (undocumented) // @public (undocumented)
export class StyleProp<Type> implements T.Validatable<Type> { export class StyleProp<Type> implements T.Validatable<Type> {
protected constructor(id: string, defaultValue: Type, type: T.Validatable<Type>); constructor(id: string, defaultValue: Type, type: T.Validatable<Type>);
// (undocumented) // (undocumented)
readonly defaultValue: Type; readonly defaultValue: Type;
// (undocumented) // (undocumented)

View file

@ -16,7 +16,7 @@ export class StyleProp<Type> implements T.Validatable<Type> {
return new EnumStyleProp<Values[number]>(uniqueId, defaultValue, values) return new EnumStyleProp<Values[number]>(uniqueId, defaultValue, values)
} }
protected constructor( constructor(
readonly id: string, readonly id: string,
readonly defaultValue: Type, readonly defaultValue: Type,
readonly type: T.Validatable<Type> readonly type: T.Validatable<Type>

View file

@ -4626,6 +4626,8 @@ __metadata:
"@testing-library/react": ^14.0.0 "@testing-library/react": ^14.0.0
"@tldraw/editor": "workspace:*" "@tldraw/editor": "workspace:*"
"@tldraw/polyfills": "workspace:*" "@tldraw/polyfills": "workspace:*"
"@tldraw/primitives": "workspace:*"
"@tldraw/store": "workspace:*"
"@tldraw/ui": "workspace:*" "@tldraw/ui": "workspace:*"
chokidar-cli: ^3.0.0 chokidar-cli: ^3.0.0
jest-canvas-mock: ^2.4.0 jest-canvas-mock: ^2.4.0