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:
parent
38d74a9ff0
commit
57bb341593
63 changed files with 6422 additions and 6370 deletions
|
@ -11,6 +11,126 @@ keywords:
|
|||
- 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
|
|
@ -10,8 +10,8 @@ import {
|
|||
} from '@tldraw/tldraw'
|
||||
import { T } from '@tldraw/validate'
|
||||
|
||||
// Define a style that can be used across multiple shapes. The ID (myApp:filter) must be globally
|
||||
// unique, so we recommend prefixing it with a namespace.
|
||||
// Define a style that can be used across multiple shapes.
|
||||
// The ID (myApp:filter) must be globally unique, so we recommend prefixing it with a namespace.
|
||||
export const MyFilterStyle = StyleProp.defineEnum('myApp:filter', {
|
||||
defaultValue: 'none',
|
||||
values: ['none', 'invert', 'grayscale', 'blur'],
|
||||
|
@ -30,16 +30,13 @@ export type CardShape = TLBaseShape<
|
|||
>
|
||||
|
||||
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'] {
|
||||
override getDefaultProps(): CardShape['props'] {
|
||||
return {
|
||||
w: 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) {
|
||||
const bounds = this.bounds(shape)
|
||||
const bounds = this.editor.getBounds(shape)
|
||||
|
||||
return (
|
||||
<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) {
|
||||
return <rect width={shape.props.w} height={shape.props.h} />
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { TLUiMenuGroup, Tldraw, menuItem, toolbarItem, useEditor } from '@tldraw/tldraw'
|
||||
import { Tldraw } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
import { TLUiOverrides } from '@tldraw/ui/src/lib/overrides'
|
||||
import { track } from 'signia-react'
|
||||
import { CardShape, MyFilterStyle } from './CardShape'
|
||||
import { CardShape } from './CardShape'
|
||||
import { FilterStyleUi } from './FilterStyleUi'
|
||||
import { uiOverrides } from './ui-overrides'
|
||||
|
||||
const shapes = [CardShape]
|
||||
|
||||
|
@ -13,72 +13,10 @@ export default function CustomStylesExample() {
|
|||
autoFocus
|
||||
persistenceKey="custom-styles-example"
|
||||
shapes={shapes}
|
||||
overrides={cardToolMenuItems}
|
||||
overrides={uiOverrides}
|
||||
>
|
||||
<FilterStyleUi />
|
||||
</Tldraw>
|
||||
</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
|
||||
},
|
||||
}
|
||||
|
|
29
apps/examples/src/16-custom-styles/FilterStyleUi.tsx
Normal file
29
apps/examples/src/16-custom-styles/FilterStyleUi.tsx
Normal 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>
|
||||
)
|
||||
})
|
28
apps/examples/src/16-custom-styles/ui-overrides.ts
Normal file
28
apps/examples/src/16-custom-styles/ui-overrides.ts
Normal 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
|
||||
},
|
||||
}
|
|
@ -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,
|
||||
})
|
17
apps/examples/src/3-custom-config/CardShape/CardShape.ts
Normal file
17
apps/examples/src/3-custom-config/CardShape/CardShape.ts
Normal 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,
|
||||
})
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
|
@ -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,
|
||||
}
|
|
@ -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
|
||||
}
|
||||
>
|
|
@ -1,50 +1,17 @@
|
|||
import { TLUiMenuGroup, Tldraw, menuItem, toolbarItem } from '@tldraw/tldraw'
|
||||
import { Tldraw } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
import { CardShape } from './CardShape'
|
||||
|
||||
const shapes = [CardShape]
|
||||
import { customShapes } from './custom-shapes'
|
||||
import { uiOverrides } from './ui-overrides'
|
||||
|
||||
export default function CustomConfigExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw
|
||||
autoFocus
|
||||
shapes={shapes}
|
||||
overrides={{
|
||||
// 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
|
||||
},
|
||||
}}
|
||||
// Pass in the array of custom shape definitions
|
||||
shapes={customShapes}
|
||||
// Pass in any overrides to the user interface
|
||||
overrides={uiOverrides}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
3
apps/examples/src/3-custom-config/custom-shapes.ts
Normal file
3
apps/examples/src/3-custom-config/custom-shapes.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { CardShape } from '../16-custom-styles/CardShape'
|
||||
|
||||
export const customShapes = [CardShape]
|
33
apps/examples/src/3-custom-config/ui-overrides.ts
Normal file
33
apps/examples/src/3-custom-config/ui-overrides.ts
Normal 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
|
||||
},
|
||||
}
|
|
@ -6,7 +6,7 @@ export class ErrorShapeUtil extends BaseBoxShapeUtil<ErrorShape> {
|
|||
static override type = 'error' as const
|
||||
override type = 'error' as const
|
||||
|
||||
defaultProps() {
|
||||
getDefaultProps() {
|
||||
return { message: 'Error!', w: 100, h: 100 }
|
||||
}
|
||||
component(shape: ErrorShape) {
|
||||
|
|
|
@ -12,6 +12,7 @@ import { Box2dModel } from '@tldraw/tlschema';
|
|||
import { Computed } from 'signia';
|
||||
import { ComputedCache } from '@tldraw/store';
|
||||
import { CubicSpline2d } from '@tldraw/primitives';
|
||||
import { defineMigrations } from '@tldraw/store';
|
||||
import { EASINGS } from '@tldraw/primitives';
|
||||
import { EmbedDefinition } from '@tldraw/tlschema';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
|
@ -111,15 +112,13 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
// (undocumented)
|
||||
component(shape: TLArrowShape): JSX.Element | null;
|
||||
// (undocumented)
|
||||
defaultProps(): TLArrowShape['props'];
|
||||
// (undocumented)
|
||||
getArrowInfo(shape: TLArrowShape): ArrowInfo | undefined;
|
||||
// (undocumented)
|
||||
getBounds(shape: TLArrowShape): Box2d;
|
||||
// (undocumented)
|
||||
getCenter(shape: TLArrowShape): Vec2d;
|
||||
// (undocumented)
|
||||
getEditingBounds: (shape: TLArrowShape) => Box2d;
|
||||
getDefaultProps(): TLArrowShape['props'];
|
||||
// (undocumented)
|
||||
getHandles(shape: TLArrowShape): TLHandle[];
|
||||
// (undocumented)
|
||||
|
@ -205,7 +204,7 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
|
|||
// (undocumented)
|
||||
component(shape: TLBookmarkShape): JSX.Element;
|
||||
// (undocumented)
|
||||
defaultProps(): TLBookmarkShape['props'];
|
||||
getDefaultProps(): TLBookmarkShape['props'];
|
||||
// (undocumented)
|
||||
hideSelectionBoundsBg: () => boolean;
|
||||
// (undocumented)
|
||||
|
@ -293,6 +292,8 @@ export const defaultShapes: readonly [TLShapeInfo<TLDrawShape>, TLShapeInfo<TLGe
|
|||
// @public (undocumented)
|
||||
export const defaultTools: TLStateNodeConstructor[];
|
||||
|
||||
export { defineMigrations }
|
||||
|
||||
// @public (undocumented)
|
||||
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)
|
||||
component(shape: TLDrawShape): JSX.Element;
|
||||
// (undocumented)
|
||||
defaultProps(): TLDrawShape['props'];
|
||||
// (undocumented)
|
||||
expandSelectionOutlinePx(shape: TLDrawShape): number;
|
||||
// (undocumented)
|
||||
getBounds(shape: TLDrawShape): Box2d;
|
||||
// (undocumented)
|
||||
getCenter(shape: TLDrawShape): Vec2d;
|
||||
// (undocumented)
|
||||
getDefaultProps(): TLDrawShape['props'];
|
||||
// (undocumented)
|
||||
getOutline(shape: TLDrawShape): Vec2d[];
|
||||
// (undocumented)
|
||||
hideResizeHandles: (shape: TLDrawShape) => boolean;
|
||||
|
@ -450,20 +451,24 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
}[];
|
||||
getAssetById(id: TLAssetId): TLAsset | undefined;
|
||||
getAssetBySrc(src: string): TLBookmarkAsset | TLImageAsset | TLVideoAsset | undefined;
|
||||
getBounds(shape: TLShape): Box2d;
|
||||
getBoundsById(id: TLShapeId): Box2d | undefined;
|
||||
getBounds<T extends TLShape>(shape: T): Box2d;
|
||||
getBoundsById<T extends TLShape>(id: T['id']): Box2d | undefined;
|
||||
getClipPathById(id: TLShapeId): string | undefined;
|
||||
getContainer: () => HTMLElement;
|
||||
getContent(ids?: TLShapeId[]): TLContent | undefined;
|
||||
getDeltaInParentSpace(shape: TLShape, delta: VecLike): Vec2d;
|
||||
getDeltaInShapeSpace(shape: TLShape, delta: VecLike): Vec2d;
|
||||
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;
|
||||
getMaskedPageBounds(shape: TLShape): Box2d | undefined;
|
||||
getMaskedPageBoundsById(id: TLShapeId): Box2d | undefined;
|
||||
getOutermostSelectableShape(shape: TLShape, filter?: (shape: TLShape) => boolean): TLShape;
|
||||
getOutline(shape: TLShape): Vec2d[];
|
||||
getOutline<T extends TLShape>(shape: T): Vec2d[];
|
||||
getOutlineById(id: TLShapeId): Vec2d[];
|
||||
getOutlineSegments<T extends TLShape>(shape: T): Vec2d[][];
|
||||
getOutlineSegmentsById(id: TLShapeId): Vec2d[][];
|
||||
getPageBounds(shape: TLShape): Box2d | undefined;
|
||||
getPageBoundsById(id: TLShapeId): Box2d | undefined;
|
||||
getPageById(id: TLPageId): TLPage | undefined;
|
||||
|
@ -644,6 +649,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
setCurrentPageId(pageId: TLPageId, { stopFollowing }?: TLViewportOptions): this;
|
||||
setCursor(cursor: Partial<TLCursor>): this;
|
||||
setDarkMode(isDarkMode: boolean): this;
|
||||
setDevicePixelRatio(dpr: number): this;
|
||||
setEditingId(id: null | TLShapeId): this;
|
||||
setErasingIds(ids?: TLShapeId[]): this;
|
||||
setFocusLayer(next: null | TLShapeId): this;
|
||||
|
@ -651,9 +657,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
setGridMode(isGridMode: boolean): this;
|
||||
setHintingIds(ids: TLShapeId[]): this;
|
||||
setHoveredId(id?: null | TLShapeId): this;
|
||||
setInstancePageState(partial: Partial<TLInstancePageState>, ephemeral?: boolean): void;
|
||||
setLocale(locale: string): void;
|
||||
setOpacity(opacity: number, ephemeral?: boolean, squashing?: boolean): this;
|
||||
setPageState(partial: Partial<TLInstancePageState>, ephemeral?: boolean): void;
|
||||
setPenMode(isPenMode: boolean): this;
|
||||
// @internal (undocumented)
|
||||
setProjectName(name: string): void;
|
||||
|
@ -685,7 +691,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
stopFollowingUser(): this;
|
||||
readonly store: TLStore;
|
||||
stretchShapes(operation: 'horizontal' | 'vertical', ids?: TLShapeId[]): this;
|
||||
textMeasure: TextManager;
|
||||
readonly textMeasure: TextManager;
|
||||
toggleLock(ids?: TLShapeId[]): this;
|
||||
undo(): HistoryManager<this>;
|
||||
ungroupShapes(ids?: TLShapeId[]): this;
|
||||
|
@ -727,7 +733,7 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
|
|||
// (undocumented)
|
||||
component(shape: TLEmbedShape): JSX.Element;
|
||||
// (undocumented)
|
||||
defaultProps(): TLEmbedShape['props'];
|
||||
getDefaultProps(): TLEmbedShape['props'];
|
||||
// (undocumented)
|
||||
hideSelectionBoundsBg: TLShapeUtilFlag<TLEmbedShape>;
|
||||
// (undocumented)
|
||||
|
@ -792,7 +798,7 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
|||
// (undocumented)
|
||||
component(shape: TLFrameShape): JSX.Element;
|
||||
// (undocumented)
|
||||
defaultProps(): TLFrameShape['props'];
|
||||
getDefaultProps(): TLFrameShape['props'];
|
||||
// (undocumented)
|
||||
indicator(shape: TLFrameShape): JSX.Element;
|
||||
// (undocumented)
|
||||
|
@ -821,12 +827,12 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
// (undocumented)
|
||||
component(shape: TLGeoShape): JSX.Element;
|
||||
// (undocumented)
|
||||
defaultProps(): TLGeoShape['props'];
|
||||
// (undocumented)
|
||||
getBounds(shape: TLGeoShape): Box2d;
|
||||
// (undocumented)
|
||||
getCenter(shape: TLGeoShape): Vec2d;
|
||||
// (undocumented)
|
||||
getDefaultProps(): TLGeoShape['props'];
|
||||
// (undocumented)
|
||||
getOutline(shape: TLGeoShape): Vec2d[];
|
||||
// (undocumented)
|
||||
hitTestLineSegment(shape: TLGeoShape, A: VecLike, B: VecLike): boolean;
|
||||
|
@ -1041,12 +1047,12 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
|
|||
// (undocumented)
|
||||
component(shape: TLGroupShape): JSX.Element | null;
|
||||
// (undocumented)
|
||||
defaultProps(): TLGroupShape['props'];
|
||||
// (undocumented)
|
||||
getBounds(shape: TLGroupShape): Box2d;
|
||||
// (undocumented)
|
||||
getCenter(shape: TLGroupShape): Vec2d;
|
||||
// (undocumented)
|
||||
getDefaultProps(): TLGroupShape['props'];
|
||||
// (undocumented)
|
||||
getOutline(shape: TLGroupShape): Vec2d[];
|
||||
// (undocumented)
|
||||
hideSelectionBoundsBg: () => boolean;
|
||||
|
@ -1082,9 +1088,9 @@ export const HighlightShape: TLShapeInfo<TLHighlightShape>;
|
|||
// @public (undocumented)
|
||||
export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
||||
// (undocumented)
|
||||
component(shape: TLHighlightShape): JSX.Element;
|
||||
backgroundComponent(shape: TLHighlightShape): JSX.Element;
|
||||
// (undocumented)
|
||||
defaultProps(): TLHighlightShape['props'];
|
||||
component(shape: TLHighlightShape): JSX.Element;
|
||||
// (undocumented)
|
||||
expandSelectionOutlinePx(shape: TLHighlightShape): number;
|
||||
// (undocumented)
|
||||
|
@ -1092,6 +1098,8 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
|||
// (undocumented)
|
||||
getCenter(shape: TLHighlightShape): Vec2d;
|
||||
// (undocumented)
|
||||
getDefaultProps(): TLHighlightShape['props'];
|
||||
// (undocumented)
|
||||
getOutline(shape: TLHighlightShape): Vec2d[];
|
||||
// (undocumented)
|
||||
hideResizeHandles: (shape: TLHighlightShape) => boolean;
|
||||
|
@ -1110,8 +1118,6 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
|||
// (undocumented)
|
||||
onResize: TLOnResizeHandler<TLHighlightShape>;
|
||||
// (undocumented)
|
||||
renderBackground(shape: TLHighlightShape): JSX.Element;
|
||||
// (undocumented)
|
||||
toBackgroundSvg(shape: TLHighlightShape, font: string | undefined, colors: TLExportColors): SVGPathElement;
|
||||
// (undocumented)
|
||||
toSvg(shape: TLHighlightShape, _font: string | undefined, colors: TLExportColors): SVGPathElement;
|
||||
|
@ -1135,7 +1141,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
|||
// (undocumented)
|
||||
component(shape: TLImageShape): JSX.Element;
|
||||
// (undocumented)
|
||||
defaultProps(): TLImageShape['props'];
|
||||
getDefaultProps(): TLImageShape['props'];
|
||||
// (undocumented)
|
||||
indicator(shape: TLImageShape): JSX.Element | null;
|
||||
// (undocumented)
|
||||
|
@ -1182,11 +1188,9 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
// (undocumented)
|
||||
component(shape: TLLineShape): JSX.Element | undefined;
|
||||
// (undocumented)
|
||||
defaultProps(): TLLineShape['props'];
|
||||
// (undocumented)
|
||||
getBounds(shape: TLLineShape): Box2d;
|
||||
// (undocumented)
|
||||
getCenter(shape: TLLineShape): Vec2d;
|
||||
getDefaultProps(): TLLineShape['props'];
|
||||
// (undocumented)
|
||||
getHandles(shape: TLLineShape): TLHandle[];
|
||||
// (undocumented)
|
||||
|
@ -1650,12 +1654,12 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
// (undocumented)
|
||||
component(shape: TLNoteShape): JSX.Element;
|
||||
// (undocumented)
|
||||
defaultProps(): TLNoteShape['props'];
|
||||
// (undocumented)
|
||||
getBounds(shape: TLNoteShape): Box2d;
|
||||
// (undocumented)
|
||||
getCenter(_shape: TLNoteShape): Vec2d;
|
||||
// (undocumented)
|
||||
getDefaultProps(): TLNoteShape['props'];
|
||||
// (undocumented)
|
||||
getHeight(shape: TLNoteShape): number;
|
||||
// (undocumented)
|
||||
getOutline(shape: TLNoteShape): Vec2d[];
|
||||
|
@ -1820,7 +1824,8 @@ export function setUserPreferences(user: TLUserPreferences): void;
|
|||
// @public (undocumented)
|
||||
export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||
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;
|
||||
canCrop: TLShapeUtilFlag<Shape>;
|
||||
canDropShapes(shape: Shape, shapes: TLShape[]): boolean;
|
||||
|
@ -1832,20 +1837,18 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
canUnmount: TLShapeUtilFlag<Shape>;
|
||||
center(shape: Shape): Vec2d;
|
||||
abstract component(shape: Shape): any;
|
||||
abstract defaultProps(): Shape['props'];
|
||||
// (undocumented)
|
||||
editor: Editor;
|
||||
// @internal (undocumented)
|
||||
expandSelectionOutlinePx(shape: Shape): number;
|
||||
protected abstract getBounds(shape: Shape): Box2d;
|
||||
abstract getCenter(shape: Shape): Vec2d;
|
||||
getEditingBounds: (shape: Shape) => Box2d;
|
||||
protected getHandles?(shape: Shape): TLHandle[];
|
||||
protected abstract getOutline(shape: Shape): Vec2d[];
|
||||
protected getOutlineSegments(shape: Shape): Vec2d[][];
|
||||
abstract getBounds(shape: Shape): Box2d;
|
||||
getCenter(shape: Shape): Vec2d;
|
||||
abstract getDefaultProps(): Shape['props'];
|
||||
getHandles?(shape: Shape): TLHandle[];
|
||||
getOutline(shape: Shape): Vec2d[];
|
||||
getOutlineSegments(shape: Shape): Vec2d[][];
|
||||
// (undocumented)
|
||||
getStyleIfExists<T>(style: StyleProp<T>, shape: Shape | TLShapePartial<Shape>): T | undefined;
|
||||
handles(shape: Shape): TLHandle[];
|
||||
// (undocumented)
|
||||
hasStyle(style: StyleProp<unknown>): boolean;
|
||||
hideResizeHandles: TLShapeUtilFlag<Shape>;
|
||||
|
@ -1884,12 +1887,8 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
onTranslate?: TLOnTranslateHandler<Shape>;
|
||||
onTranslateEnd?: TLOnTranslateEndHandler<Shape>;
|
||||
onTranslateStart?: TLOnTranslateStartHandler<Shape>;
|
||||
outline(shape: Shape): Vec2d[];
|
||||
outlineSegments(shape: Shape): Vec2d[][];
|
||||
// @internal
|
||||
providesBackgroundForChildren(shape: Shape): boolean;
|
||||
// @internal
|
||||
renderBackground?(shape: Shape): any;
|
||||
// (undocumented)
|
||||
setStyleInPartial<T>(style: StyleProp<T>, shape: TLShapePartial<Shape>, value: T): TLShapePartial<Shape>;
|
||||
snapPoints(shape: Shape): Vec2d[];
|
||||
|
@ -2020,11 +2019,9 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|||
// (undocumented)
|
||||
component(shape: TLTextShape): JSX.Element;
|
||||
// (undocumented)
|
||||
defaultProps(): TLTextShape['props'];
|
||||
// (undocumented)
|
||||
getBounds(shape: TLTextShape): Box2d;
|
||||
// (undocumented)
|
||||
getCenter(shape: TLTextShape): Vec2d;
|
||||
getDefaultProps(): TLTextShape['props'];
|
||||
// (undocumented)
|
||||
getMinDimensions(shape: TLTextShape): {
|
||||
height: number;
|
||||
|
@ -2489,7 +2486,7 @@ export type TLOnHandleChangeHandler<T extends TLShape> = (shape: T, info: {
|
|||
export type TLOnResizeEndHandler<T extends TLShape> = TLEventChangeHandler<T>;
|
||||
|
||||
// @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)
|
||||
export type TLOnResizeStartHandler<T extends TLShape> = TLEventStartHandler<T>;
|
||||
|
@ -2763,7 +2760,7 @@ export class VideoShapeUtil extends BaseBoxShapeUtil<TLVideoShape> {
|
|||
// (undocumented)
|
||||
component(shape: TLVideoShape): JSX.Element;
|
||||
// (undocumented)
|
||||
defaultProps(): TLVideoShape['props'];
|
||||
getDefaultProps(): TLVideoShape['props'];
|
||||
// (undocumented)
|
||||
indicator(shape: TLVideoShape): JSX.Element;
|
||||
// (undocumented)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
// eslint-disable-next-line local/no-export-star
|
||||
export * from '@tldraw/indices'
|
||||
export { defineMigrations } from '@tldraw/store'
|
||||
// eslint-disable-next-line local/no-export-star
|
||||
export * from '@tldraw/tlschema'
|
||||
export { getHashForString } from '@tldraw/utils'
|
||||
|
|
|
@ -207,8 +207,7 @@ const HandlesWrapper = track(function HandlesWrapper() {
|
|||
|
||||
if (!(onlySelectedShape && shouldDisplayHandles)) return null
|
||||
|
||||
const util = editor.getShapeUtil(onlySelectedShape)
|
||||
const handles = util.handles?.(onlySelectedShape)
|
||||
const handles = editor.getHandles(onlySelectedShape)
|
||||
|
||||
if (!handles) return null
|
||||
|
||||
|
|
|
@ -83,8 +83,7 @@ export const Shape = track(function Shape({
|
|||
const shape = editor.getShapeById(id)
|
||||
if (!shape) return null
|
||||
|
||||
const util = editor.getShapeUtil(shape)
|
||||
const bounds = util.bounds(shape)
|
||||
const bounds = editor.getBounds(shape)
|
||||
setProperty('width', Math.ceil(bounds.width) + 'px')
|
||||
setProperty('height', Math.ceil(bounds.height) + 'px')
|
||||
},
|
||||
|
@ -106,7 +105,7 @@ export const Shape = track(function Shape({
|
|||
|
||||
return (
|
||||
<>
|
||||
{util.renderBackground && (
|
||||
{util.backgroundComponent && (
|
||||
<div
|
||||
ref={backgroundContainerRef}
|
||||
className="tl-shape tl-shape-background"
|
||||
|
@ -137,7 +136,7 @@ export const Shape = track(function Shape({
|
|||
onPointerLeave={events.onPointerLeave}
|
||||
>
|
||||
{isCulled && util.canUnmount(shape) ? (
|
||||
<CulledShape shape={shape} util={util} />
|
||||
<CulledShape shape={shape} />
|
||||
) : (
|
||||
<OptionalErrorBoundary
|
||||
fallback={ShapeErrorFallback ? (error) => <ShapeErrorFallback error={error} /> : null}
|
||||
|
@ -168,14 +167,16 @@ const InnerShapeBackground = React.memo(
|
|||
shape: 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
|
||||
)
|
||||
|
||||
const CulledShape = React.memo(
|
||||
function CulledShap<T extends TLShape>({ shape, util }: { shape: T; util: ShapeUtil<T> }) {
|
||||
const bounds = util.bounds(shape)
|
||||
function CulledShape<T extends TLShape>({ shape }: { shape: T }) {
|
||||
const editor = useEditor()
|
||||
const bounds = editor.getBounds(shape)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="tl-shape__culled"
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -235,7 +235,7 @@ export class ExternalContentManager {
|
|||
const p =
|
||||
point ?? (editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.viewportPageCenter)
|
||||
|
||||
const defaultProps = editor.getShapeUtil(TextShapeUtil).defaultProps()
|
||||
const defaultProps = editor.getShapeUtil(TextShapeUtil).getDefaultProps()
|
||||
|
||||
const textToPaste = stripTrailingWhitespace(
|
||||
stripCommonMinimumIndentation(replaceTabsWithSpaces(text))
|
||||
|
|
|
@ -17,15 +17,15 @@ export abstract class BaseBoxShapeUtil<Shape extends TLBaseBoxShape> extends Sha
|
|||
}
|
||||
|
||||
override getOutline(shape: Shape) {
|
||||
return this.bounds(shape).corners
|
||||
return this.editor.getBounds(shape).corners
|
||||
}
|
||||
|
||||
hitTestPoint(shape: Shape, point: VecLike): boolean {
|
||||
return pointInPolygon(point, this.outline(shape))
|
||||
override hitTestPoint(shape: Shape, point: VecLike): boolean {
|
||||
return pointInPolygon(point, this.editor.getOutline(shape))
|
||||
}
|
||||
|
||||
hitTestLineSegment(shape: Shape, A: VecLike, B: VecLike): boolean {
|
||||
const outline = this.outline(shape)
|
||||
override hitTestLineSegment(shape: Shape, A: VecLike, B: VecLike): boolean {
|
||||
const outline = this.editor.getOutline(shape)
|
||||
|
||||
for (let i = 0; i < outline.length; i++) {
|
||||
const C = outline[i]
|
||||
|
@ -36,7 +36,7 @@ export abstract class BaseBoxShapeUtil<Shape extends TLBaseBoxShape> extends Sha
|
|||
return false
|
||||
}
|
||||
|
||||
onResize: TLOnResizeHandler<any> = (shape, info) => {
|
||||
override onResize: TLOnResizeHandler<any> = (shape, info) => {
|
||||
return resizeBox(shape, info)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Box2d, linesIntersect, Vec2d, VecLike } from '@tldraw/primitives'
|
||||
import { ComputedCache } from '@tldraw/store'
|
||||
import { StyleProp, TLHandle, TLShape, TLShapePartial, TLUnknownShape } from '@tldraw/tlschema'
|
||||
import { computed, EMPTY_ARRAY } from 'signia'
|
||||
import type { Editor } from '../Editor'
|
||||
import { TLResizeHandle } from '../types/selection-types'
|
||||
import { TLExportColors } from './shared/TLExportColors'
|
||||
|
@ -67,6 +65,29 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -118,14 +139,16 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
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.
|
||||
*
|
||||
* @public
|
||||
* @internal
|
||||
*/
|
||||
getEditingBounds = (shape: Shape) => {
|
||||
return this.bounds(shape)
|
||||
providesBackgroundForChildren(shape: Shape): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -170,36 +193,13 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
* @param shape - The shape.
|
||||
* @internal
|
||||
*/
|
||||
renderBackground?(shape: Shape): any
|
||||
backgroundComponent?(shape: Shape): any
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @public
|
||||
*/
|
||||
protected 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
|
||||
}
|
||||
getHandles?(shape: Shape): TLHandle[]
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @public
|
||||
*/
|
||||
protected getOutlineSegments(shape: Shape): Vec2d[][] {
|
||||
return [this.outline(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
|
||||
getOutlineSegments(shape: Shape): Vec2d[][] {
|
||||
return [this.editor.getOutline(shape)]
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -276,52 +240,16 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
* @param shape - The shape.
|
||||
* @public
|
||||
*/
|
||||
protected 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)
|
||||
})
|
||||
}
|
||||
abstract getBounds(shape: Shape): Box2d
|
||||
|
||||
/**
|
||||
* Get the cached bounds for the shape.
|
||||
* Get the shape's (not cached) outline.
|
||||
*
|
||||
* @param shape - The shape.
|
||||
* @public
|
||||
*/
|
||||
bounds(shape: Shape): Box2d {
|
||||
const result = this.boundsCache.get(shape.id) ?? new Box2d()
|
||||
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
|
||||
getOutline(shape: Shape): Vec2d[] {
|
||||
return this.editor.getBounds(shape).corners
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -331,7 +259,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
* @public
|
||||
*/
|
||||
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.
|
||||
* @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.
|
||||
|
@ -403,6 +333,11 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
colors: TLExportColors
|
||||
): SVGElement | Promise<SVGElement> | null
|
||||
|
||||
/** @internal */
|
||||
expandSelectionOutlinePx(shape: Shape): number {
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether a point intersects the shape.
|
||||
*
|
||||
|
@ -412,7 +347,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
* @public
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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++) {
|
||||
const C = outline[i]
|
||||
|
@ -436,24 +371,6 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
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
|
||||
|
||||
/**
|
||||
|
@ -745,7 +662,7 @@ export type TLResizeInfo<T extends TLShape> = {
|
|||
export type TLOnResizeHandler<T extends TLShape> = (
|
||||
shape: T,
|
||||
info: TLResizeInfo<T>
|
||||
) => Partial<TLShapePartial<T>> | undefined | void
|
||||
) => Omit<TLShapePartial<T>, 'id' | 'type'> | undefined | void
|
||||
|
||||
/** @public */
|
||||
export type TLOnResizeStartHandler<T extends TLShape> = TLEventStartHandler<T>
|
||||
|
|
|
@ -73,7 +73,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
override hideSelectionBoundsFg: TLShapeUtilFlag<TLArrowShape> = () => true
|
||||
override hideSelectionBoundsBg: TLShapeUtilFlag<TLArrowShape> = () => true
|
||||
|
||||
override defaultProps(): TLArrowShape['props'] {
|
||||
override getDefaultProps(): TLArrowShape['props'] {
|
||||
return {
|
||||
dash: 'draw',
|
||||
size: 'm',
|
||||
|
@ -91,7 +91,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
}
|
||||
|
||||
getCenter(shape: TLArrowShape): Vec2d {
|
||||
return this.bounds(shape).center
|
||||
return this.editor.getBounds(shape).center
|
||||
}
|
||||
|
||||
getBounds(shape: TLArrowShape) {
|
||||
|
@ -292,7 +292,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
|
||||
if (util.isClosed(hitShape)) {
|
||||
// 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
|
||||
|
@ -533,7 +533,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
}
|
||||
|
||||
hitTestPoint(shape: TLArrowShape, point: VecLike): boolean {
|
||||
const outline = this.outline(shape)
|
||||
const outline = this.editor.getOutline(shape)
|
||||
const zoomLevel = this.editor.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 {
|
||||
const outline = this.outline(shape)
|
||||
const outline = this.editor.getOutline(shape)
|
||||
|
||||
for (let i = 0; i < outline.length - 1; i++) {
|
||||
const C = outline[i]
|
||||
|
@ -571,7 +571,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
) && !this.editor.isReadOnly
|
||||
|
||||
const info = this.getArrowInfo(shape)
|
||||
const bounds = this.bounds(shape)
|
||||
const bounds = this.editor.getBounds(shape)
|
||||
const labelSize = this.getLabelBounds(shape)
|
||||
|
||||
// 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 info = this.getArrowInfo(shape)
|
||||
const bounds = this.bounds(shape)
|
||||
const bounds = this.editor.getBounds(shape)
|
||||
const labelSize = this.getLabelBounds(shape)
|
||||
|
||||
if (!info) return null
|
||||
|
@ -844,7 +844,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
@computed get labelBoundsCache(): ComputedCache<Box2d | null, TLArrowShape> {
|
||||
return this.editor.store.createComputedCache('labelBoundsCache', (shape) => {
|
||||
const info = this.getArrowInfo(shape)
|
||||
const bounds = this.bounds(shape)
|
||||
const bounds = this.editor.getBounds(shape)
|
||||
const { text, font, size } = shape.props
|
||||
|
||||
if (!info) return null
|
||||
|
@ -901,10 +901,6 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
return this.labelBoundsCache.get(shape.id) || null
|
||||
}
|
||||
|
||||
getEditingBounds = (shape: TLArrowShape): Box2d => {
|
||||
return this.getLabelBounds(shape) ?? new Box2d()
|
||||
}
|
||||
|
||||
onEditEnd: TLOnEditEndHandler<TLArrowShape> = (shape) => {
|
||||
const {
|
||||
id,
|
||||
|
@ -941,7 +937,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
// Arrowhead end path
|
||||
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 maskId = (shape.id + '_clip').replace(':', '_')
|
||||
|
|
|
@ -74,7 +74,7 @@ export function getCurvedArrowInfo(editor: Editor, shape: TLArrowShape, extraBen
|
|||
const endInStartShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, endInPageSpace)
|
||||
const centerInStartShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, centerInPageSpace)
|
||||
|
||||
const isClosed = startShapeInfo.util.isClosed(startShapeInfo.shape)
|
||||
const { isClosed } = startShapeInfo
|
||||
const fn = isClosed ? intersectCirclePolygon : intersectCirclePolyline
|
||||
|
||||
let point: VecLike | undefined
|
||||
|
@ -82,7 +82,7 @@ export function getCurvedArrowInfo(editor: Editor, shape: TLArrowShape, extraBen
|
|||
let intersections = fn(
|
||||
centerInStartShapeLocalSpace,
|
||||
handleArc.radius,
|
||||
startShapeInfo.util.outline(startShapeInfo.shape)
|
||||
editor.getOutline(startShapeInfo.shape)
|
||||
)
|
||||
|
||||
if (intersections) {
|
||||
|
@ -150,7 +150,7 @@ export function getCurvedArrowInfo(editor: Editor, shape: TLArrowShape, extraBen
|
|||
const endInEndShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, endInPageSpace)
|
||||
const centerInEndShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, centerInPageSpace)
|
||||
|
||||
const isClosed = endShapeInfo.util.isClosed(endShapeInfo.shape)
|
||||
const isClosed = endShapeInfo.isClosed
|
||||
const fn = isClosed ? intersectCirclePolygon : intersectCirclePolyline
|
||||
|
||||
const angleToMiddle = Vec2d.Angle(handleArc.center, middle)
|
||||
|
@ -162,7 +162,7 @@ export function getCurvedArrowInfo(editor: Editor, shape: TLArrowShape, extraBen
|
|||
let intersections = fn(
|
||||
centerInEndShapeLocalSpace,
|
||||
handleArc.radius,
|
||||
endShapeInfo.util.outline(endShapeInfo.shape)
|
||||
editor.getOutline(endShapeInfo.shape)
|
||||
)
|
||||
|
||||
if (intersections) {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Matrix2d, Vec2d } from '@tldraw/primitives'
|
||||
import { TLArrowShape, TLArrowShapeTerminal, TLShape } from '@tldraw/tlschema'
|
||||
import { Editor } from '../../../Editor'
|
||||
import { ShapeUtil } from '../../ShapeUtil'
|
||||
|
||||
export function getIsArrowStraight(shape: TLArrowShape) {
|
||||
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> = {
|
||||
shape: T
|
||||
util: ShapeUtil<T>
|
||||
didIntersect: boolean
|
||||
isExact: boolean
|
||||
isClosed: boolean
|
||||
transform: Matrix2d
|
||||
outline: Vec2d[]
|
||||
}
|
||||
|
||||
export function getBoundShapeInfoForTerminal(
|
||||
|
@ -29,10 +29,11 @@ export function getBoundShapeInfoForTerminal(
|
|||
|
||||
return {
|
||||
shape,
|
||||
util,
|
||||
transform,
|
||||
isClosed: util.isClosed(shape),
|
||||
isExact: terminal.isExact,
|
||||
didIntersect: false,
|
||||
outline: editor.getOutline(shape),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -67,14 +67,14 @@ export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): Arrow
|
|||
if (endShapeInfo.didIntersect && !startShapeInfo.didIntersect) {
|
||||
// ...and if only the end shape intersected, then make it
|
||||
// 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))
|
||||
}
|
||||
} else if (!endShapeInfo.didIntersect) {
|
||||
// ...and if only the end shape intersected, or if neither
|
||||
// shape intersected, then make it a short arrow starting
|
||||
// at the start shape intersection.
|
||||
if (endShapeInfo.util.isClosed(endShapeInfo.shape)) {
|
||||
if (endShapeInfo.isClosed) {
|
||||
b.setTo(Vec2d.Nudge(a, b, minDist))
|
||||
}
|
||||
}
|
||||
|
@ -179,10 +179,10 @@ function updateArrowheadPointWithBoundShape(
|
|||
const targetFrom = Matrix2d.applyToPoint(Matrix2d.Inverse(targetShapeInfo.transform), pageFrom)
|
||||
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 intersection = fn(targetFrom, targetTo, targetShapeInfo.util.outline(targetShapeInfo.shape))
|
||||
const intersection = fn(targetFrom, targetTo, targetShapeInfo.outline)
|
||||
|
||||
let targetInt: VecLike | undefined
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ export class Pointing extends StateNode {
|
|||
const shape = this.editor.getShapeById<TLArrowShape>(id)
|
||||
if (!shape) return
|
||||
|
||||
const handles = util.handles?.(shape)
|
||||
const handles = this.editor.getHandles(shape)
|
||||
|
||||
if (handles) {
|
||||
// start precise
|
||||
|
@ -82,8 +82,7 @@ export class Pointing extends StateNode {
|
|||
if (!this.shape) return
|
||||
|
||||
if (this.editor.inputs.isDragging) {
|
||||
const util = this.editor.getShapeUtil(this.shape)
|
||||
const handles = util.handles?.(this.shape)
|
||||
const handles = this.editor.getHandles(this.shape)
|
||||
|
||||
if (!handles) {
|
||||
this.editor.bailToMark('creating')
|
||||
|
@ -96,8 +95,6 @@ export class Pointing extends StateNode {
|
|||
|
||||
if (!shape) return
|
||||
|
||||
const handles = util.handles(shape)
|
||||
|
||||
if (handles) {
|
||||
const { x, y } = this.editor.getPointInShapeSpace(
|
||||
shape,
|
||||
|
|
|
@ -23,7 +23,7 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
|
|||
override hideSelectionBoundsBg = () => true
|
||||
override hideSelectionBoundsFg = () => true
|
||||
|
||||
override defaultProps(): TLBookmarkShape['props'] {
|
||||
override getDefaultProps(): TLBookmarkShape['props'] {
|
||||
return {
|
||||
url: '',
|
||||
w: 300,
|
||||
|
|
|
@ -30,7 +30,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
|||
hideSelectionBoundsBg = (shape: TLDrawShape) => getIsDot(shape)
|
||||
hideSelectionBoundsFg = (shape: TLDrawShape) => getIsDot(shape)
|
||||
|
||||
override defaultProps(): TLDrawShape['props'] {
|
||||
override getDefaultProps(): TLDrawShape['props'] {
|
||||
return {
|
||||
segments: [],
|
||||
color: 'black',
|
||||
|
@ -46,7 +46,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
|||
isClosed = (shape: TLDrawShape) => shape.props.isClosed
|
||||
|
||||
getBounds(shape: TLDrawShape) {
|
||||
return Box2d.FromPoints(this.outline(shape))
|
||||
return Box2d.FromPoints(this.editor.getOutline(shape))
|
||||
}
|
||||
|
||||
getOutline(shape: TLDrawShape) {
|
||||
|
@ -54,11 +54,11 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
|||
}
|
||||
|
||||
getCenter(shape: TLDrawShape): Vec2d {
|
||||
return this.bounds(shape).center
|
||||
return this.editor.getBounds(shape).center
|
||||
}
|
||||
|
||||
hitTestPoint(shape: TLDrawShape, point: VecLike): boolean {
|
||||
const outline = this.outline(shape)
|
||||
const outline = this.editor.getOutline(shape)
|
||||
const zoomLevel = this.editor.zoomLevel
|
||||
const offsetDist = STROKE_SIZES[shape.props.size] / zoomLevel
|
||||
|
||||
|
@ -72,7 +72,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
|||
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++) {
|
||||
const C = outline[i]
|
||||
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 {
|
||||
const outline = this.outline(shape)
|
||||
const outline = this.editor.getOutline(shape)
|
||||
|
||||
if (shape.props.segments.length === 1 && shape.props.segments[0].points.length < 4) {
|
||||
const zoomLevel = this.editor.zoomLevel
|
||||
|
|
|
@ -38,7 +38,7 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
|
|||
return !!getEmbedInfo(shape.props.url)?.definition?.doesResize
|
||||
}
|
||||
|
||||
override defaultProps(): TLEmbedShape['props'] {
|
||||
override getDefaultProps(): TLEmbedShape['props'] {
|
||||
return {
|
||||
w: 300,
|
||||
h: 300,
|
||||
|
|
|
@ -18,12 +18,12 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
|||
|
||||
override canEdit = () => true
|
||||
|
||||
override defaultProps(): TLFrameShape['props'] {
|
||||
override getDefaultProps(): TLFrameShape['props'] {
|
||||
return { w: 160 * 2, h: 90 * 2, name: '' }
|
||||
}
|
||||
|
||||
override component(shape: TLFrameShape) {
|
||||
const bounds = this.bounds(shape)
|
||||
const bounds = this.editor.getBounds(shape)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -137,7 +137,7 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
|||
}
|
||||
|
||||
indicator(shape: TLFrameShape) {
|
||||
const bounds = this.bounds(shape)
|
||||
const bounds = this.editor.getBounds(shape)
|
||||
|
||||
return (
|
||||
<rect
|
||||
|
|
|
@ -50,7 +50,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
|
||||
canEdit = () => true
|
||||
|
||||
override defaultProps(): TLGeoShape['props'] {
|
||||
override getDefaultProps(): TLGeoShape['props'] {
|
||||
return {
|
||||
w: 100,
|
||||
h: 100,
|
||||
|
@ -70,7 +70,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
}
|
||||
|
||||
hitTestLineSegment(shape: TLGeoShape, A: VecLike, B: VecLike): boolean {
|
||||
const outline = this.outline(shape)
|
||||
const outline = this.editor.getOutline(shape)
|
||||
|
||||
// Check the outline
|
||||
for (let i = 0; i < outline.length; i++) {
|
||||
|
@ -91,7 +91,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
}
|
||||
|
||||
hitTestPoint(shape: TLGeoShape, point: VecLike): boolean {
|
||||
const outline = this.outline(shape)
|
||||
const outline = this.editor.getOutline(shape)
|
||||
|
||||
if (shape.props.fill === 'none') {
|
||||
const zoomLevel = this.editor.zoomLevel
|
||||
|
@ -397,7 +397,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
break
|
||||
}
|
||||
default: {
|
||||
const outline = this.outline(shape)
|
||||
const outline = this.editor.getOutline(shape)
|
||||
const lines = getLines(shape.props, strokeWidth)
|
||||
|
||||
if (dash === 'solid' || (dash === 'draw' && forceSolid)) {
|
||||
|
@ -479,7 +479,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
}
|
||||
|
||||
default: {
|
||||
const outline = this.outline(shape)
|
||||
const outline = this.editor.getOutline(shape)
|
||||
let path: string
|
||||
|
||||
if (props.dash === 'draw' && !forceSolid) {
|
||||
|
@ -591,7 +591,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
break
|
||||
}
|
||||
default: {
|
||||
const outline = this.outline(shape)
|
||||
const outline = this.editor.getOutline(shape)
|
||||
const lines = getLines(shape.props, strokeWidth)
|
||||
|
||||
switch (props.dash) {
|
||||
|
@ -635,7 +635,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
}
|
||||
|
||||
if (props.text) {
|
||||
const bounds = this.bounds(shape)
|
||||
const bounds = this.editor.getBounds(shape)
|
||||
|
||||
const rootTextElm = getTextLabelSvgElement({
|
||||
editor: this.editor,
|
||||
|
|
|
@ -15,7 +15,7 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
|
|||
|
||||
canBind = () => false
|
||||
|
||||
defaultProps(): TLGroupShape['props'] {
|
||||
getDefaultProps(): TLGroupShape['props'] {
|
||||
return {}
|
||||
}
|
||||
|
||||
|
@ -36,11 +36,11 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
|
|||
}
|
||||
|
||||
getCenter(shape: TLGroupShape): Vec2d {
|
||||
return this.bounds(shape).center
|
||||
return this.editor.getBounds(shape).center
|
||||
}
|
||||
|
||||
getOutline(shape: TLGroupShape): Vec2d[] {
|
||||
return this.bounds(shape).corners
|
||||
return this.editor.getBounds(shape).corners
|
||||
}
|
||||
|
||||
component(shape: TLGroupShape) {
|
||||
|
@ -71,7 +71,7 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
|
|||
return null
|
||||
}
|
||||
|
||||
const bounds = this.bounds(shape)
|
||||
const bounds = this.editor.getBounds(shape)
|
||||
|
||||
return (
|
||||
<SVGContainer id={shape.id}>
|
||||
|
@ -86,7 +86,7 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
|
|||
camera: { z: zoomLevel },
|
||||
} = this.editor
|
||||
|
||||
const bounds = this.bounds(shape)
|
||||
const bounds = this.editor.getBounds(shape)
|
||||
|
||||
return <DashedOutlineBox className="" bounds={bounds} zoomLevel={zoomLevel} />
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
|||
hideSelectionBoundsBg = (shape: TLHighlightShape) => getIsDot(shape)
|
||||
hideSelectionBoundsFg = (shape: TLHighlightShape) => getIsDot(shape)
|
||||
|
||||
override defaultProps(): TLHighlightShape['props'] {
|
||||
override getDefaultProps(): TLHighlightShape['props'] {
|
||||
return {
|
||||
segments: [],
|
||||
color: 'black',
|
||||
|
@ -33,7 +33,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
|||
}
|
||||
|
||||
getBounds(shape: TLHighlightShape) {
|
||||
return Box2d.FromPoints(this.outline(shape))
|
||||
return Box2d.FromPoints(this.editor.getOutline(shape))
|
||||
}
|
||||
|
||||
getOutline(shape: TLHighlightShape) {
|
||||
|
@ -41,11 +41,11 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
|||
}
|
||||
|
||||
getCenter(shape: TLHighlightShape): Vec2d {
|
||||
return this.bounds(shape).center
|
||||
return this.editor.getBounds(shape).center
|
||||
}
|
||||
|
||||
hitTestPoint(shape: TLHighlightShape, point: VecLike): boolean {
|
||||
const outline = this.outline(shape)
|
||||
const outline = this.editor.getOutline(shape)
|
||||
const zoomLevel = this.editor.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++) {
|
||||
const C = outline[i]
|
||||
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 {
|
||||
const outline = this.outline(shape)
|
||||
const outline = this.editor.getOutline(shape)
|
||||
|
||||
if (shape.props.segments.length === 1 && shape.props.segments[0].points.length < 4) {
|
||||
const zoomLevel = this.editor.zoomLevel
|
||||
|
@ -102,7 +102,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
|||
)
|
||||
}
|
||||
|
||||
renderBackground(shape: TLHighlightShape) {
|
||||
backgroundComponent(shape: TLHighlightShape) {
|
||||
return (
|
||||
<HighlightRenderer
|
||||
strokeWidth={getStrokeWidth(shape)}
|
||||
|
|
|
@ -54,7 +54,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
|||
override isAspectRatioLocked = () => true
|
||||
override canCrop = () => true
|
||||
|
||||
override defaultProps(): TLImageShape['props'] {
|
||||
override getDefaultProps(): TLImageShape['props'] {
|
||||
return {
|
||||
w: 100,
|
||||
h: 100,
|
||||
|
|
|
@ -35,7 +35,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
override hideSelectionBoundsFg = () => true
|
||||
override isClosed = () => false
|
||||
|
||||
override defaultProps(): TLLineShape['props'] {
|
||||
override getDefaultProps(): TLLineShape['props'] {
|
||||
return {
|
||||
dash: 'draw',
|
||||
size: 'm',
|
||||
|
@ -68,10 +68,6 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
return spline.bounds
|
||||
}
|
||||
|
||||
getCenter(shape: TLLineShape) {
|
||||
return this.bounds(shape).center
|
||||
}
|
||||
|
||||
getHandles(shape: TLLineShape) {
|
||||
return handlesCache.get(shape.props, () => {
|
||||
const handles = shape.props.handles
|
||||
|
@ -174,11 +170,11 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
hitTestPoint(shape: TLLineShape, point: Vec2d): boolean {
|
||||
const zoomLevel = this.editor.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 {
|
||||
return intersectLineSegmentPolyline(A, B, this.outline(shape)) !== null
|
||||
return intersectLineSegmentPolyline(A, B, this.editor.getOutline(shape)) !== null
|
||||
}
|
||||
|
||||
component(shape: TLLineShape) {
|
||||
|
|
|
@ -29,7 +29,8 @@ export class Pointing extends StateNode {
|
|||
|
||||
// if user is holding shift then we are adding points to an existing line
|
||||
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 endHandle = vertexHandles[vertexHandles.length - 1]
|
||||
|
@ -96,8 +97,7 @@ export class Pointing extends StateNode {
|
|||
if (!this.shape) return
|
||||
|
||||
if (this.editor.inputs.isDragging) {
|
||||
const util = this.editor.getShapeUtil(this.shape)
|
||||
const handles = util.handles?.(this.shape)
|
||||
const handles = this.editor.getHandles(this.shape)
|
||||
if (!handles) {
|
||||
this.editor.bailToMark('creating')
|
||||
throw Error('No handles found')
|
||||
|
|
|
@ -19,7 +19,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
hideSelectionBoundsBg = () => true
|
||||
hideSelectionBoundsFg = () => true
|
||||
|
||||
defaultProps(): TLNoteShape['props'] {
|
||||
getDefaultProps(): TLNoteShape['props'] {
|
||||
return {
|
||||
color: 'black',
|
||||
size: 'm',
|
||||
|
@ -42,7 +42,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
}
|
||||
|
||||
getOutline(shape: TLNoteShape) {
|
||||
return this.bounds(shape).corners
|
||||
return this.editor.getBounds(shape).corners
|
||||
}
|
||||
|
||||
getCenter(_shape: TLNoteShape) {
|
||||
|
@ -106,7 +106,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
}
|
||||
|
||||
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')
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { TLNoteShape, createShapeId } from '@tldraw/tlschema'
|
||||
import { NoteShapeUtil } from '../../../shapes/note/NoteShapeUtil'
|
||||
import { StateNode } from '../../../tools/StateNode'
|
||||
import { TLEventHandlers, TLInterruptEvent, TLPointerEventInfo } from '../../../types/event-types'
|
||||
|
||||
|
@ -97,9 +96,8 @@ export class Pointing extends StateNode {
|
|||
true
|
||||
)
|
||||
|
||||
const util = this.editor.getShapeUtil(NoteShapeUtil)
|
||||
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
|
||||
this.editor.updateShapes([
|
||||
|
|
|
@ -24,7 +24,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|||
|
||||
isAspectRatioLocked: TLShapeUtilFlag<TLTextShape> = () => true
|
||||
|
||||
defaultProps(): TLTextShape['props'] {
|
||||
getDefaultProps(): TLTextShape['props'] {
|
||||
return {
|
||||
color: 'black',
|
||||
size: 'm',
|
||||
|
@ -63,7 +63,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|||
}
|
||||
|
||||
getOutline(shape: TLTextShape) {
|
||||
const bounds = this.bounds(shape)
|
||||
const bounds = this.editor.getBounds(shape)
|
||||
|
||||
return [
|
||||
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) {
|
||||
const {
|
||||
id,
|
||||
|
@ -150,12 +145,12 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|||
}
|
||||
|
||||
indicator(shape: TLTextShape) {
|
||||
const bounds = this.bounds(shape)
|
||||
const bounds = this.getBounds(shape)
|
||||
return <rect width={toDomPrecision(bounds.width)} height={toDomPrecision(bounds.height)} />
|
||||
}
|
||||
|
||||
toSvg(shape: TLTextShape, font: string | undefined, colors: TLExportColors) {
|
||||
const bounds = this.bounds(shape)
|
||||
const bounds = this.getBounds(shape)
|
||||
const text = shape.props.text
|
||||
|
||||
const width = bounds.width / (shape.props.scale ?? 1)
|
||||
|
@ -204,7 +199,11 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|||
const { initialBounds, initialShape, scaleX, handle } = info
|
||||
|
||||
if (info.mode === 'scale_shape' || (handle !== 'right' && handle !== 'left')) {
|
||||
return resizeScaled(shape, info)
|
||||
return {
|
||||
id: shape.id,
|
||||
type: shape.type,
|
||||
...resizeScaled(shape, info),
|
||||
}
|
||||
} else {
|
||||
const prevWidth = initialBounds.width
|
||||
let nextWidth = prevWidth * scaleX
|
||||
|
@ -227,6 +226,8 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|||
const { x, y } = offset.rot(shape.rotation).add(initialShape)
|
||||
|
||||
return {
|
||||
id: shape.id,
|
||||
type: shape.type,
|
||||
x,
|
||||
y,
|
||||
props: {
|
||||
|
|
|
@ -16,7 +16,7 @@ export class VideoShapeUtil extends BaseBoxShapeUtil<TLVideoShape> {
|
|||
override canEdit = () => true
|
||||
override isAspectRatioLocked = () => true
|
||||
|
||||
override defaultProps(): TLVideoShape['props'] {
|
||||
override getDefaultProps(): TLVideoShape['props'] {
|
||||
return {
|
||||
w: 100,
|
||||
h: 100,
|
||||
|
|
|
@ -94,7 +94,7 @@ export class Pointing extends StateNode {
|
|||
])
|
||||
|
||||
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))
|
||||
|
||||
this.editor.updateShapes<TLBaseBoxShape>([
|
||||
|
|
|
@ -5,6 +5,7 @@ let editor: TestEditor
|
|||
|
||||
const ids = {
|
||||
box1: createShapeId('box1'),
|
||||
line1: createShapeId('line1'),
|
||||
embed1: createShapeId('embed1'),
|
||||
}
|
||||
|
||||
|
@ -135,7 +136,8 @@ describe('PointingHandle', () => {
|
|||
|
||||
describe('DraggingHandle', () => {
|
||||
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, {
|
||||
target: 'handle',
|
||||
shape,
|
||||
|
@ -149,7 +151,8 @@ describe('DraggingHandle', () => {
|
|||
})
|
||||
|
||||
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, {
|
||||
target: 'handle',
|
||||
|
|
|
@ -58,7 +58,7 @@ export class DraggingHandle extends StateNode {
|
|||
this.editor.setCursor({ type: isCreating ? 'cross' : 'grabbing', rotation: 0 })
|
||||
|
||||
// <!-- 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)
|
||||
|
||||
// Find the adjacent handle
|
||||
|
@ -227,14 +227,14 @@ export class DraggingHandle extends StateNode {
|
|||
|
||||
// Get all the outline segments from the shape
|
||||
const additionalSegments = util
|
||||
.outlineSegments(shape)
|
||||
.getOutlineSegments(shape)
|
||||
.map((segment) => Matrix2d.applyToPoints(pageTransform, segment))
|
||||
|
||||
// We want to skip the segments that include the handle, so
|
||||
// find the index of the handle that shares the same index property
|
||||
// as the initial dragging handle; this catches a quirk of create handles
|
||||
const handleIndex = util
|
||||
.handles(shape)
|
||||
const handleIndex = editor
|
||||
.getHandles(shape)!
|
||||
.filter(({ type }) => type === 'vertex')
|
||||
.sort(sortByIndex)
|
||||
.findIndex(({ index }) => initialHandle.index === index)
|
||||
|
|
|
@ -405,7 +405,7 @@ export class Resizing extends StateNode {
|
|||
|
||||
return {
|
||||
shape,
|
||||
bounds: util.bounds(shape),
|
||||
bounds: this.editor.getBounds(shape),
|
||||
pageTransform,
|
||||
pageRotation: Matrix2d.Decompose(pageTransform!).rotation,
|
||||
isAspectRatioLocked: util.isAspectRatioLocked(shape),
|
||||
|
|
|
@ -11,6 +11,21 @@ export function useDocumentEvents() {
|
|||
|
||||
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(() => {
|
||||
if (!isAppFocused) return
|
||||
|
||||
|
|
|
@ -7,8 +7,7 @@ import { useEditor } from './useEditor'
|
|||
|
||||
function getHandle(editor: Editor, id: TLShapeId, handleId: string) {
|
||||
const shape = editor.getShapeById<TLArrowShape | TLLineShape>(id)!
|
||||
const util = editor.getShapeUtil(shape)
|
||||
const handles = util.handles(shape)
|
||||
const handles = editor.getHandles(shape)!
|
||||
return { shape, handle: handles.find((h) => h.id === handleId) }
|
||||
}
|
||||
|
||||
|
|
|
@ -455,7 +455,7 @@ describe('getShapeUtil', () => {
|
|||
class MyFakeShapeUtil extends BaseBoxShapeUtil<any> {
|
||||
static type = 'fake'
|
||||
|
||||
defaultProps() {
|
||||
getDefaultProps() {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
component() {
|
||||
|
@ -475,7 +475,7 @@ describe('getShapeUtil', () => {
|
|||
class MyFakeGeoShapeUtil extends BaseBoxShapeUtil<any> {
|
||||
static type = 'geo'
|
||||
|
||||
defaultProps() {
|
||||
getDefaultProps() {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
component() {
|
||||
|
|
|
@ -275,7 +275,7 @@ describe('Custom shapes', () => {
|
|||
override canResize = (_shape: CardShape) => true
|
||||
override canBind = (_shape: CardShape) => true
|
||||
|
||||
override defaultProps(): CardShape['props'] {
|
||||
override getDefaultProps(): CardShape['props'] {
|
||||
return {
|
||||
w: 300,
|
||||
h: 300,
|
||||
|
@ -283,7 +283,7 @@ describe('Custom shapes', () => {
|
|||
}
|
||||
|
||||
component(shape: CardShape) {
|
||||
const bounds = this.bounds(shape)
|
||||
const bounds = this.getBounds(shape)
|
||||
|
||||
return (
|
||||
<HTMLContainer
|
||||
|
|
|
@ -33,7 +33,7 @@ describe('creating frames', () => {
|
|||
editor.setSelectedTool('frame')
|
||||
editor.pointerDown(100, 100).pointerUp(100, 100)
|
||||
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({
|
||||
x: 100 - w / 2,
|
||||
y: 100 - h / 2,
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
} from '@tldraw/tlschema'
|
||||
import { assert, compact } from '@tldraw/utils'
|
||||
import { ArrowShapeTool } from '../../editor/shapes/arrow/ArrowShapeTool'
|
||||
import { ArrowShapeUtil } from '../../editor/shapes/arrow/ArrowShapeUtil'
|
||||
import { DrawShapeTool } from '../../editor/shapes/draw/DrawShapeTool'
|
||||
import { GroupShapeUtil } from '../../editor/shapes/group/GroupShapeUtil'
|
||||
import { LineShapeTool } from '../../editor/shapes/line/LineShapeTool'
|
||||
|
@ -1679,10 +1678,7 @@ describe('moving handles within a group', () => {
|
|||
editor.pointerDown(60, 60, {
|
||||
target: 'handle',
|
||||
shape: arrow,
|
||||
handle: editor
|
||||
.getShapeUtil(ArrowShapeUtil)
|
||||
.handles(arrow)
|
||||
.find((h) => h.id === 'end'),
|
||||
handle: editor.getHandles<TLArrowShape>(arrow)!.find((h) => h.id === 'end'),
|
||||
})
|
||||
|
||||
editor.expectToBeIn('select.pointing_handle')
|
||||
|
|
|
@ -14,7 +14,7 @@ type __TopLeftSnapOnlyShape = any
|
|||
class __TopLeftSnapOnlyShapeUtil extends ShapeUtil<__TopLeftSnapOnlyShape> {
|
||||
static override type = '__test_top_left_snap_only' as const
|
||||
|
||||
defaultProps(): __TopLeftSnapOnlyShape['props'] {
|
||||
getDefaultProps(): __TopLeftSnapOnlyShape['props'] {
|
||||
return { width: 10, height: 10 }
|
||||
}
|
||||
getBounds(shape: __TopLeftSnapOnlyShape): Box2d {
|
||||
|
|
|
@ -550,7 +550,7 @@ export function buildFromV1Document(editor: Editor, document: LegacyTldrawDocume
|
|||
y: bounds.minY + bounds.height * ny,
|
||||
})
|
||||
|
||||
const handles = util.handles(v2ShapeFresh)
|
||||
const handles = editor.getHandles(v2ShapeFresh)!
|
||||
|
||||
const change = util.onHandleChange!(v2ShapeFresh, {
|
||||
handle: {
|
||||
|
|
|
@ -12,6 +12,7 @@ export function Tldraw(props: TldrawEditorProps & TldrawUiProps): JSX.Element;
|
|||
|
||||
|
||||
export * from "@tldraw/editor";
|
||||
export * from "@tldraw/primitives";
|
||||
export * from "@tldraw/ui";
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
|
|
@ -46,6 +46,8 @@
|
|||
"dependencies": {
|
||||
"@tldraw/editor": "workspace:*",
|
||||
"@tldraw/polyfills": "workspace:*",
|
||||
"@tldraw/primitives": "workspace:*",
|
||||
"@tldraw/store": "workspace:*",
|
||||
"@tldraw/ui": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
/** @internal */
|
||||
import '@tldraw/polyfills'
|
||||
|
||||
// eslint-disable-next-line local/no-export-star
|
||||
export * from '@tldraw/editor'
|
||||
// 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 { Tldraw } from './lib/Tldraw'
|
||||
|
|
|
@ -706,7 +706,7 @@ export type ShapeProps<Shape extends TLBaseShape<any, any>> = {
|
|||
|
||||
// @public (undocumented)
|
||||
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)
|
||||
readonly defaultValue: Type;
|
||||
// (undocumented)
|
||||
|
|
|
@ -16,7 +16,7 @@ export class StyleProp<Type> implements T.Validatable<Type> {
|
|||
return new EnumStyleProp<Values[number]>(uniqueId, defaultValue, values)
|
||||
}
|
||||
|
||||
protected constructor(
|
||||
constructor(
|
||||
readonly id: string,
|
||||
readonly defaultValue: Type,
|
||||
readonly type: T.Validatable<Type>
|
||||
|
|
|
@ -4626,6 +4626,8 @@ __metadata:
|
|||
"@testing-library/react": ^14.0.0
|
||||
"@tldraw/editor": "workspace:*"
|
||||
"@tldraw/polyfills": "workspace:*"
|
||||
"@tldraw/primitives": "workspace:*"
|
||||
"@tldraw/store": "workspace:*"
|
||||
"@tldraw/ui": "workspace:*"
|
||||
chokidar-cli: ^3.0.0
|
||||
jest-canvas-mock: ^2.4.0
|
||||
|
|
Loading…
Reference in a new issue