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
|
- 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'
|
} from '@tldraw/tldraw'
|
||||||
import { T } from '@tldraw/validate'
|
import { T } from '@tldraw/validate'
|
||||||
|
|
||||||
// Define a style that can be used across multiple shapes. The ID (myApp:filter) must be globally
|
// Define a style that can be used across multiple shapes.
|
||||||
// unique, so we recommend prefixing it with a namespace.
|
// The ID (myApp:filter) must be globally unique, so we recommend prefixing it with a namespace.
|
||||||
export const MyFilterStyle = StyleProp.defineEnum('myApp:filter', {
|
export const MyFilterStyle = StyleProp.defineEnum('myApp:filter', {
|
||||||
defaultValue: 'none',
|
defaultValue: 'none',
|
||||||
values: ['none', 'invert', 'grayscale', 'blur'],
|
values: ['none', 'invert', 'grayscale', 'blur'],
|
||||||
|
@ -30,16 +30,13 @@ export type CardShape = TLBaseShape<
|
||||||
>
|
>
|
||||||
|
|
||||||
export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
|
export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
|
||||||
// Id — the shape util's id
|
|
||||||
static override type = 'card' as const
|
static override type = 'card' as const
|
||||||
|
|
||||||
// Flags — there are a LOT of other flags!
|
|
||||||
override isAspectRatioLocked = (_shape: CardShape) => false
|
override isAspectRatioLocked = (_shape: CardShape) => false
|
||||||
override canResize = (_shape: CardShape) => true
|
override canResize = (_shape: CardShape) => true
|
||||||
override canBind = (_shape: CardShape) => true
|
override canBind = (_shape: CardShape) => true
|
||||||
|
|
||||||
// Default props — used for shapes created with the tool
|
override getDefaultProps(): CardShape['props'] {
|
||||||
override defaultProps(): CardShape['props'] {
|
|
||||||
return {
|
return {
|
||||||
w: 300,
|
w: 300,
|
||||||
h: 300,
|
h: 300,
|
||||||
|
@ -48,9 +45,8 @@ export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The React component that will be rendered for the shape; can return any HTML elements here
|
|
||||||
component(shape: CardShape) {
|
component(shape: CardShape) {
|
||||||
const bounds = this.bounds(shape)
|
const bounds = this.editor.getBounds(shape)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HTMLContainer
|
<HTMLContainer
|
||||||
|
@ -71,7 +67,7 @@ export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The indicator shown when hovering over a shape or when it's selected; must return only SVG elements here
|
// Indicator — used when hovering over a shape or when it's selected; must return only SVG elements here
|
||||||
indicator(shape: CardShape) {
|
indicator(shape: CardShape) {
|
||||||
return <rect width={shape.props.w} height={shape.props.h} />
|
return <rect width={shape.props.w} height={shape.props.h} />
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { TLUiMenuGroup, Tldraw, menuItem, toolbarItem, useEditor } from '@tldraw/tldraw'
|
import { Tldraw } from '@tldraw/tldraw'
|
||||||
import '@tldraw/tldraw/tldraw.css'
|
import '@tldraw/tldraw/tldraw.css'
|
||||||
import { TLUiOverrides } from '@tldraw/ui/src/lib/overrides'
|
import { CardShape } from './CardShape'
|
||||||
import { track } from 'signia-react'
|
import { FilterStyleUi } from './FilterStyleUi'
|
||||||
import { CardShape, MyFilterStyle } from './CardShape'
|
import { uiOverrides } from './ui-overrides'
|
||||||
|
|
||||||
const shapes = [CardShape]
|
const shapes = [CardShape]
|
||||||
|
|
||||||
|
@ -13,72 +13,10 @@ export default function CustomStylesExample() {
|
||||||
autoFocus
|
autoFocus
|
||||||
persistenceKey="custom-styles-example"
|
persistenceKey="custom-styles-example"
|
||||||
shapes={shapes}
|
shapes={shapes}
|
||||||
overrides={cardToolMenuItems}
|
overrides={uiOverrides}
|
||||||
>
|
>
|
||||||
<FilterStyleUi />
|
<FilterStyleUi />
|
||||||
</Tldraw>
|
</Tldraw>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const FilterStyleUi = track(function FilterStyleUi() {
|
|
||||||
const editor = useEditor()
|
|
||||||
const filterStyle = editor.sharedStyles.get(MyFilterStyle)
|
|
||||||
|
|
||||||
// if the filter style isn't in sharedStyles, it means it's not relevant to the current tool/selection
|
|
||||||
if (!filterStyle) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ position: 'absolute', zIndex: 300, top: 64, left: 12 }}>
|
|
||||||
filter:{' '}
|
|
||||||
<select
|
|
||||||
value={filterStyle.type === 'mixed' ? 'mixed' : filterStyle.value}
|
|
||||||
onChange={(e) => editor.setStyle(MyFilterStyle, e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="mixed" disabled>
|
|
||||||
Mixed
|
|
||||||
</option>
|
|
||||||
<option value="none">None</option>
|
|
||||||
<option value="invert">Invert</option>
|
|
||||||
<option value="grayscale">Grayscale</option>
|
|
||||||
<option value="blur">Blur</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const cardToolMenuItems: TLUiOverrides = {
|
|
||||||
// In order for our custom tool to show up in the UI...
|
|
||||||
// We need to add it to the tools list. This "toolItem"
|
|
||||||
// has information about its icon, label, keyboard shortcut,
|
|
||||||
// and what to do when it's selected.
|
|
||||||
tools(editor, tools) {
|
|
||||||
tools.card = {
|
|
||||||
id: 'card',
|
|
||||||
icon: 'color',
|
|
||||||
label: 'Card' as any,
|
|
||||||
kbd: 'c',
|
|
||||||
readonlyOk: false,
|
|
||||||
onSelect: () => {
|
|
||||||
editor.setSelectedTool('card')
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return tools
|
|
||||||
},
|
|
||||||
toolbar(_app, toolbar, { tools }) {
|
|
||||||
// The toolbar is an array of items. We can add it to the
|
|
||||||
// end of the array or splice it in, then return the array.
|
|
||||||
toolbar.splice(4, 0, toolbarItem(tools.card))
|
|
||||||
return toolbar
|
|
||||||
},
|
|
||||||
keyboardShortcutsMenu(_app, keyboardShortcutsMenu, { tools }) {
|
|
||||||
// Same for the keyboard shortcuts menu, but this menu contains
|
|
||||||
// both items and groups. We want to find the "Tools" group and
|
|
||||||
// add it to that before returning the array.
|
|
||||||
const toolsGroup = keyboardShortcutsMenu.find(
|
|
||||||
(group) => group.id === 'shortcuts-dialog.tools'
|
|
||||||
) as TLUiMenuGroup
|
|
||||||
toolsGroup.children.push(menuItem(tools.card))
|
|
||||||
return keyboardShortcutsMenu
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
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 '@tldraw/tldraw/tldraw.css'
|
||||||
import { CardShape } from './CardShape'
|
import { customShapes } from './custom-shapes'
|
||||||
|
import { uiOverrides } from './ui-overrides'
|
||||||
const shapes = [CardShape]
|
|
||||||
|
|
||||||
export default function CustomConfigExample() {
|
export default function CustomConfigExample() {
|
||||||
return (
|
return (
|
||||||
<div className="tldraw__editor">
|
<div className="tldraw__editor">
|
||||||
<Tldraw
|
<Tldraw
|
||||||
autoFocus
|
autoFocus
|
||||||
shapes={shapes}
|
// Pass in the array of custom shape definitions
|
||||||
overrides={{
|
shapes={customShapes}
|
||||||
// In order for our custom tool to show up in the UI...
|
// Pass in any overrides to the user interface
|
||||||
// We need to add it to the tools list. This "toolItem"
|
overrides={uiOverrides}
|
||||||
// has information about its icon, label, keyboard shortcut,
|
|
||||||
// and what to do when it's selected.
|
|
||||||
tools(editor, tools) {
|
|
||||||
tools.card = {
|
|
||||||
id: 'card',
|
|
||||||
icon: 'color',
|
|
||||||
label: 'Card' as any,
|
|
||||||
kbd: 'c',
|
|
||||||
readonlyOk: false,
|
|
||||||
onSelect: () => {
|
|
||||||
editor.setSelectedTool('card')
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return tools
|
|
||||||
},
|
|
||||||
toolbar(_app, toolbar, { tools }) {
|
|
||||||
// The toolbar is an array of items. We can add it to the
|
|
||||||
// end of the array or splice it in, then return the array.
|
|
||||||
toolbar.splice(4, 0, toolbarItem(tools.card))
|
|
||||||
return toolbar
|
|
||||||
},
|
|
||||||
keyboardShortcutsMenu(_app, keyboardShortcutsMenu, { tools }) {
|
|
||||||
// Same for the keyboard shortcuts menu, but this menu contains
|
|
||||||
// both items and groups. We want to find the "Tools" group and
|
|
||||||
// add it to that before returning the array.
|
|
||||||
const toolsGroup = keyboardShortcutsMenu.find(
|
|
||||||
(group) => group.id === 'shortcuts-dialog.tools'
|
|
||||||
) as TLUiMenuGroup
|
|
||||||
toolsGroup.children.push(menuItem(tools.card))
|
|
||||||
return keyboardShortcutsMenu
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
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
|
static override type = 'error' as const
|
||||||
override type = 'error' as const
|
override type = 'error' as const
|
||||||
|
|
||||||
defaultProps() {
|
getDefaultProps() {
|
||||||
return { message: 'Error!', w: 100, h: 100 }
|
return { message: 'Error!', w: 100, h: 100 }
|
||||||
}
|
}
|
||||||
component(shape: ErrorShape) {
|
component(shape: ErrorShape) {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { Box2dModel } from '@tldraw/tlschema';
|
||||||
import { Computed } from 'signia';
|
import { Computed } from 'signia';
|
||||||
import { ComputedCache } from '@tldraw/store';
|
import { ComputedCache } from '@tldraw/store';
|
||||||
import { CubicSpline2d } from '@tldraw/primitives';
|
import { CubicSpline2d } from '@tldraw/primitives';
|
||||||
|
import { defineMigrations } from '@tldraw/store';
|
||||||
import { EASINGS } from '@tldraw/primitives';
|
import { EASINGS } from '@tldraw/primitives';
|
||||||
import { EmbedDefinition } from '@tldraw/tlschema';
|
import { EmbedDefinition } from '@tldraw/tlschema';
|
||||||
import { EventEmitter } from 'eventemitter3';
|
import { EventEmitter } from 'eventemitter3';
|
||||||
|
@ -111,15 +112,13 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
component(shape: TLArrowShape): JSX.Element | null;
|
component(shape: TLArrowShape): JSX.Element | null;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
defaultProps(): TLArrowShape['props'];
|
|
||||||
// (undocumented)
|
|
||||||
getArrowInfo(shape: TLArrowShape): ArrowInfo | undefined;
|
getArrowInfo(shape: TLArrowShape): ArrowInfo | undefined;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getBounds(shape: TLArrowShape): Box2d;
|
getBounds(shape: TLArrowShape): Box2d;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getCenter(shape: TLArrowShape): Vec2d;
|
getCenter(shape: TLArrowShape): Vec2d;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getEditingBounds: (shape: TLArrowShape) => Box2d;
|
getDefaultProps(): TLArrowShape['props'];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getHandles(shape: TLArrowShape): TLHandle[];
|
getHandles(shape: TLArrowShape): TLHandle[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -205,7 +204,7 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
component(shape: TLBookmarkShape): JSX.Element;
|
component(shape: TLBookmarkShape): JSX.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
defaultProps(): TLBookmarkShape['props'];
|
getDefaultProps(): TLBookmarkShape['props'];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hideSelectionBoundsBg: () => boolean;
|
hideSelectionBoundsBg: () => boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -293,6 +292,8 @@ export const defaultShapes: readonly [TLShapeInfo<TLDrawShape>, TLShapeInfo<TLGe
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const defaultTools: TLStateNodeConstructor[];
|
export const defaultTools: TLStateNodeConstructor[];
|
||||||
|
|
||||||
|
export { defineMigrations }
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function defineShape<T extends TLUnknownShape>(type: T['type'], opts: Omit<TLShapeInfo<T>, 'type'>): TLShapeInfo<T>;
|
export function defineShape<T extends TLUnknownShape>(type: T['type'], opts: Omit<TLShapeInfo<T>, 'type'>): TLShapeInfo<T>;
|
||||||
|
|
||||||
|
@ -313,14 +314,14 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
component(shape: TLDrawShape): JSX.Element;
|
component(shape: TLDrawShape): JSX.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
defaultProps(): TLDrawShape['props'];
|
|
||||||
// (undocumented)
|
|
||||||
expandSelectionOutlinePx(shape: TLDrawShape): number;
|
expandSelectionOutlinePx(shape: TLDrawShape): number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getBounds(shape: TLDrawShape): Box2d;
|
getBounds(shape: TLDrawShape): Box2d;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getCenter(shape: TLDrawShape): Vec2d;
|
getCenter(shape: TLDrawShape): Vec2d;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
getDefaultProps(): TLDrawShape['props'];
|
||||||
|
// (undocumented)
|
||||||
getOutline(shape: TLDrawShape): Vec2d[];
|
getOutline(shape: TLDrawShape): Vec2d[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hideResizeHandles: (shape: TLDrawShape) => boolean;
|
hideResizeHandles: (shape: TLDrawShape) => boolean;
|
||||||
|
@ -450,20 +451,24 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
}[];
|
}[];
|
||||||
getAssetById(id: TLAssetId): TLAsset | undefined;
|
getAssetById(id: TLAssetId): TLAsset | undefined;
|
||||||
getAssetBySrc(src: string): TLBookmarkAsset | TLImageAsset | TLVideoAsset | undefined;
|
getAssetBySrc(src: string): TLBookmarkAsset | TLImageAsset | TLVideoAsset | undefined;
|
||||||
getBounds(shape: TLShape): Box2d;
|
getBounds<T extends TLShape>(shape: T): Box2d;
|
||||||
getBoundsById(id: TLShapeId): Box2d | undefined;
|
getBoundsById<T extends TLShape>(id: T['id']): Box2d | undefined;
|
||||||
getClipPathById(id: TLShapeId): string | undefined;
|
getClipPathById(id: TLShapeId): string | undefined;
|
||||||
getContainer: () => HTMLElement;
|
getContainer: () => HTMLElement;
|
||||||
getContent(ids?: TLShapeId[]): TLContent | undefined;
|
getContent(ids?: TLShapeId[]): TLContent | undefined;
|
||||||
getDeltaInParentSpace(shape: TLShape, delta: VecLike): Vec2d;
|
getDeltaInParentSpace(shape: TLShape, delta: VecLike): Vec2d;
|
||||||
getDeltaInShapeSpace(shape: TLShape, delta: VecLike): Vec2d;
|
getDeltaInShapeSpace(shape: TLShape, delta: VecLike): Vec2d;
|
||||||
getDroppingShape(point: VecLike, droppingShapes?: TLShape[]): TLUnknownShape | undefined;
|
getDroppingShape(point: VecLike, droppingShapes?: TLShape[]): TLUnknownShape | undefined;
|
||||||
|
getHandles<T extends TLShape>(shape: T): TLHandle[] | undefined;
|
||||||
|
getHandlesById<T extends TLShape>(id: T['id']): TLHandle[] | undefined;
|
||||||
getHighestIndexForParent(parentId: TLPageId | TLShapeId): string;
|
getHighestIndexForParent(parentId: TLPageId | TLShapeId): string;
|
||||||
getMaskedPageBounds(shape: TLShape): Box2d | undefined;
|
getMaskedPageBounds(shape: TLShape): Box2d | undefined;
|
||||||
getMaskedPageBoundsById(id: TLShapeId): Box2d | undefined;
|
getMaskedPageBoundsById(id: TLShapeId): Box2d | undefined;
|
||||||
getOutermostSelectableShape(shape: TLShape, filter?: (shape: TLShape) => boolean): TLShape;
|
getOutermostSelectableShape(shape: TLShape, filter?: (shape: TLShape) => boolean): TLShape;
|
||||||
getOutline(shape: TLShape): Vec2d[];
|
getOutline<T extends TLShape>(shape: T): Vec2d[];
|
||||||
getOutlineById(id: TLShapeId): Vec2d[];
|
getOutlineById(id: TLShapeId): Vec2d[];
|
||||||
|
getOutlineSegments<T extends TLShape>(shape: T): Vec2d[][];
|
||||||
|
getOutlineSegmentsById(id: TLShapeId): Vec2d[][];
|
||||||
getPageBounds(shape: TLShape): Box2d | undefined;
|
getPageBounds(shape: TLShape): Box2d | undefined;
|
||||||
getPageBoundsById(id: TLShapeId): Box2d | undefined;
|
getPageBoundsById(id: TLShapeId): Box2d | undefined;
|
||||||
getPageById(id: TLPageId): TLPage | undefined;
|
getPageById(id: TLPageId): TLPage | undefined;
|
||||||
|
@ -644,6 +649,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
setCurrentPageId(pageId: TLPageId, { stopFollowing }?: TLViewportOptions): this;
|
setCurrentPageId(pageId: TLPageId, { stopFollowing }?: TLViewportOptions): this;
|
||||||
setCursor(cursor: Partial<TLCursor>): this;
|
setCursor(cursor: Partial<TLCursor>): this;
|
||||||
setDarkMode(isDarkMode: boolean): this;
|
setDarkMode(isDarkMode: boolean): this;
|
||||||
|
setDevicePixelRatio(dpr: number): this;
|
||||||
setEditingId(id: null | TLShapeId): this;
|
setEditingId(id: null | TLShapeId): this;
|
||||||
setErasingIds(ids?: TLShapeId[]): this;
|
setErasingIds(ids?: TLShapeId[]): this;
|
||||||
setFocusLayer(next: null | TLShapeId): this;
|
setFocusLayer(next: null | TLShapeId): this;
|
||||||
|
@ -651,9 +657,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
setGridMode(isGridMode: boolean): this;
|
setGridMode(isGridMode: boolean): this;
|
||||||
setHintingIds(ids: TLShapeId[]): this;
|
setHintingIds(ids: TLShapeId[]): this;
|
||||||
setHoveredId(id?: null | TLShapeId): this;
|
setHoveredId(id?: null | TLShapeId): this;
|
||||||
setInstancePageState(partial: Partial<TLInstancePageState>, ephemeral?: boolean): void;
|
|
||||||
setLocale(locale: string): void;
|
setLocale(locale: string): void;
|
||||||
setOpacity(opacity: number, ephemeral?: boolean, squashing?: boolean): this;
|
setOpacity(opacity: number, ephemeral?: boolean, squashing?: boolean): this;
|
||||||
|
setPageState(partial: Partial<TLInstancePageState>, ephemeral?: boolean): void;
|
||||||
setPenMode(isPenMode: boolean): this;
|
setPenMode(isPenMode: boolean): this;
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
setProjectName(name: string): void;
|
setProjectName(name: string): void;
|
||||||
|
@ -685,7 +691,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
stopFollowingUser(): this;
|
stopFollowingUser(): this;
|
||||||
readonly store: TLStore;
|
readonly store: TLStore;
|
||||||
stretchShapes(operation: 'horizontal' | 'vertical', ids?: TLShapeId[]): this;
|
stretchShapes(operation: 'horizontal' | 'vertical', ids?: TLShapeId[]): this;
|
||||||
textMeasure: TextManager;
|
readonly textMeasure: TextManager;
|
||||||
toggleLock(ids?: TLShapeId[]): this;
|
toggleLock(ids?: TLShapeId[]): this;
|
||||||
undo(): HistoryManager<this>;
|
undo(): HistoryManager<this>;
|
||||||
ungroupShapes(ids?: TLShapeId[]): this;
|
ungroupShapes(ids?: TLShapeId[]): this;
|
||||||
|
@ -727,7 +733,7 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
component(shape: TLEmbedShape): JSX.Element;
|
component(shape: TLEmbedShape): JSX.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
defaultProps(): TLEmbedShape['props'];
|
getDefaultProps(): TLEmbedShape['props'];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hideSelectionBoundsBg: TLShapeUtilFlag<TLEmbedShape>;
|
hideSelectionBoundsBg: TLShapeUtilFlag<TLEmbedShape>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -792,7 +798,7 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
component(shape: TLFrameShape): JSX.Element;
|
component(shape: TLFrameShape): JSX.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
defaultProps(): TLFrameShape['props'];
|
getDefaultProps(): TLFrameShape['props'];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
indicator(shape: TLFrameShape): JSX.Element;
|
indicator(shape: TLFrameShape): JSX.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -821,12 +827,12 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
component(shape: TLGeoShape): JSX.Element;
|
component(shape: TLGeoShape): JSX.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
defaultProps(): TLGeoShape['props'];
|
|
||||||
// (undocumented)
|
|
||||||
getBounds(shape: TLGeoShape): Box2d;
|
getBounds(shape: TLGeoShape): Box2d;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getCenter(shape: TLGeoShape): Vec2d;
|
getCenter(shape: TLGeoShape): Vec2d;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
getDefaultProps(): TLGeoShape['props'];
|
||||||
|
// (undocumented)
|
||||||
getOutline(shape: TLGeoShape): Vec2d[];
|
getOutline(shape: TLGeoShape): Vec2d[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hitTestLineSegment(shape: TLGeoShape, A: VecLike, B: VecLike): boolean;
|
hitTestLineSegment(shape: TLGeoShape, A: VecLike, B: VecLike): boolean;
|
||||||
|
@ -1041,12 +1047,12 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
component(shape: TLGroupShape): JSX.Element | null;
|
component(shape: TLGroupShape): JSX.Element | null;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
defaultProps(): TLGroupShape['props'];
|
|
||||||
// (undocumented)
|
|
||||||
getBounds(shape: TLGroupShape): Box2d;
|
getBounds(shape: TLGroupShape): Box2d;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getCenter(shape: TLGroupShape): Vec2d;
|
getCenter(shape: TLGroupShape): Vec2d;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
getDefaultProps(): TLGroupShape['props'];
|
||||||
|
// (undocumented)
|
||||||
getOutline(shape: TLGroupShape): Vec2d[];
|
getOutline(shape: TLGroupShape): Vec2d[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hideSelectionBoundsBg: () => boolean;
|
hideSelectionBoundsBg: () => boolean;
|
||||||
|
@ -1082,9 +1088,9 @@ export const HighlightShape: TLShapeInfo<TLHighlightShape>;
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
component(shape: TLHighlightShape): JSX.Element;
|
backgroundComponent(shape: TLHighlightShape): JSX.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
defaultProps(): TLHighlightShape['props'];
|
component(shape: TLHighlightShape): JSX.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
expandSelectionOutlinePx(shape: TLHighlightShape): number;
|
expandSelectionOutlinePx(shape: TLHighlightShape): number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -1092,6 +1098,8 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getCenter(shape: TLHighlightShape): Vec2d;
|
getCenter(shape: TLHighlightShape): Vec2d;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
getDefaultProps(): TLHighlightShape['props'];
|
||||||
|
// (undocumented)
|
||||||
getOutline(shape: TLHighlightShape): Vec2d[];
|
getOutline(shape: TLHighlightShape): Vec2d[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hideResizeHandles: (shape: TLHighlightShape) => boolean;
|
hideResizeHandles: (shape: TLHighlightShape) => boolean;
|
||||||
|
@ -1110,8 +1118,6 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
onResize: TLOnResizeHandler<TLHighlightShape>;
|
onResize: TLOnResizeHandler<TLHighlightShape>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
renderBackground(shape: TLHighlightShape): JSX.Element;
|
|
||||||
// (undocumented)
|
|
||||||
toBackgroundSvg(shape: TLHighlightShape, font: string | undefined, colors: TLExportColors): SVGPathElement;
|
toBackgroundSvg(shape: TLHighlightShape, font: string | undefined, colors: TLExportColors): SVGPathElement;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
toSvg(shape: TLHighlightShape, _font: string | undefined, colors: TLExportColors): SVGPathElement;
|
toSvg(shape: TLHighlightShape, _font: string | undefined, colors: TLExportColors): SVGPathElement;
|
||||||
|
@ -1135,7 +1141,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
component(shape: TLImageShape): JSX.Element;
|
component(shape: TLImageShape): JSX.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
defaultProps(): TLImageShape['props'];
|
getDefaultProps(): TLImageShape['props'];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
indicator(shape: TLImageShape): JSX.Element | null;
|
indicator(shape: TLImageShape): JSX.Element | null;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -1182,11 +1188,9 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
component(shape: TLLineShape): JSX.Element | undefined;
|
component(shape: TLLineShape): JSX.Element | undefined;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
defaultProps(): TLLineShape['props'];
|
|
||||||
// (undocumented)
|
|
||||||
getBounds(shape: TLLineShape): Box2d;
|
getBounds(shape: TLLineShape): Box2d;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getCenter(shape: TLLineShape): Vec2d;
|
getDefaultProps(): TLLineShape['props'];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getHandles(shape: TLLineShape): TLHandle[];
|
getHandles(shape: TLLineShape): TLHandle[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -1650,12 +1654,12 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
component(shape: TLNoteShape): JSX.Element;
|
component(shape: TLNoteShape): JSX.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
defaultProps(): TLNoteShape['props'];
|
|
||||||
// (undocumented)
|
|
||||||
getBounds(shape: TLNoteShape): Box2d;
|
getBounds(shape: TLNoteShape): Box2d;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getCenter(_shape: TLNoteShape): Vec2d;
|
getCenter(_shape: TLNoteShape): Vec2d;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
getDefaultProps(): TLNoteShape['props'];
|
||||||
|
// (undocumented)
|
||||||
getHeight(shape: TLNoteShape): number;
|
getHeight(shape: TLNoteShape): number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getOutline(shape: TLNoteShape): Vec2d[];
|
getOutline(shape: TLNoteShape): Vec2d[];
|
||||||
|
@ -1820,7 +1824,8 @@ export function setUserPreferences(user: TLUserPreferences): void;
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
constructor(editor: Editor, type: Shape['type'], styleProps: ReadonlyMap<StyleProp<unknown>, string>);
|
constructor(editor: Editor, type: Shape['type'], styleProps: ReadonlyMap<StyleProp<unknown>, string>);
|
||||||
bounds(shape: Shape): Box2d;
|
// @internal
|
||||||
|
backgroundComponent?(shape: Shape): any;
|
||||||
canBind: <K>(_shape: Shape, _otherShape?: K | undefined) => boolean;
|
canBind: <K>(_shape: Shape, _otherShape?: K | undefined) => boolean;
|
||||||
canCrop: TLShapeUtilFlag<Shape>;
|
canCrop: TLShapeUtilFlag<Shape>;
|
||||||
canDropShapes(shape: Shape, shapes: TLShape[]): boolean;
|
canDropShapes(shape: Shape, shapes: TLShape[]): boolean;
|
||||||
|
@ -1832,20 +1837,18 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
canUnmount: TLShapeUtilFlag<Shape>;
|
canUnmount: TLShapeUtilFlag<Shape>;
|
||||||
center(shape: Shape): Vec2d;
|
center(shape: Shape): Vec2d;
|
||||||
abstract component(shape: Shape): any;
|
abstract component(shape: Shape): any;
|
||||||
abstract defaultProps(): Shape['props'];
|
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
editor: Editor;
|
editor: Editor;
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
expandSelectionOutlinePx(shape: Shape): number;
|
expandSelectionOutlinePx(shape: Shape): number;
|
||||||
protected abstract getBounds(shape: Shape): Box2d;
|
abstract getBounds(shape: Shape): Box2d;
|
||||||
abstract getCenter(shape: Shape): Vec2d;
|
getCenter(shape: Shape): Vec2d;
|
||||||
getEditingBounds: (shape: Shape) => Box2d;
|
abstract getDefaultProps(): Shape['props'];
|
||||||
protected getHandles?(shape: Shape): TLHandle[];
|
getHandles?(shape: Shape): TLHandle[];
|
||||||
protected abstract getOutline(shape: Shape): Vec2d[];
|
getOutline(shape: Shape): Vec2d[];
|
||||||
protected getOutlineSegments(shape: Shape): Vec2d[][];
|
getOutlineSegments(shape: Shape): Vec2d[][];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getStyleIfExists<T>(style: StyleProp<T>, shape: Shape | TLShapePartial<Shape>): T | undefined;
|
getStyleIfExists<T>(style: StyleProp<T>, shape: Shape | TLShapePartial<Shape>): T | undefined;
|
||||||
handles(shape: Shape): TLHandle[];
|
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hasStyle(style: StyleProp<unknown>): boolean;
|
hasStyle(style: StyleProp<unknown>): boolean;
|
||||||
hideResizeHandles: TLShapeUtilFlag<Shape>;
|
hideResizeHandles: TLShapeUtilFlag<Shape>;
|
||||||
|
@ -1884,12 +1887,8 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
onTranslate?: TLOnTranslateHandler<Shape>;
|
onTranslate?: TLOnTranslateHandler<Shape>;
|
||||||
onTranslateEnd?: TLOnTranslateEndHandler<Shape>;
|
onTranslateEnd?: TLOnTranslateEndHandler<Shape>;
|
||||||
onTranslateStart?: TLOnTranslateStartHandler<Shape>;
|
onTranslateStart?: TLOnTranslateStartHandler<Shape>;
|
||||||
outline(shape: Shape): Vec2d[];
|
|
||||||
outlineSegments(shape: Shape): Vec2d[][];
|
|
||||||
// @internal
|
// @internal
|
||||||
providesBackgroundForChildren(shape: Shape): boolean;
|
providesBackgroundForChildren(shape: Shape): boolean;
|
||||||
// @internal
|
|
||||||
renderBackground?(shape: Shape): any;
|
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
setStyleInPartial<T>(style: StyleProp<T>, shape: TLShapePartial<Shape>, value: T): TLShapePartial<Shape>;
|
setStyleInPartial<T>(style: StyleProp<T>, shape: TLShapePartial<Shape>, value: T): TLShapePartial<Shape>;
|
||||||
snapPoints(shape: Shape): Vec2d[];
|
snapPoints(shape: Shape): Vec2d[];
|
||||||
|
@ -2020,11 +2019,9 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
component(shape: TLTextShape): JSX.Element;
|
component(shape: TLTextShape): JSX.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
defaultProps(): TLTextShape['props'];
|
|
||||||
// (undocumented)
|
|
||||||
getBounds(shape: TLTextShape): Box2d;
|
getBounds(shape: TLTextShape): Box2d;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getCenter(shape: TLTextShape): Vec2d;
|
getDefaultProps(): TLTextShape['props'];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getMinDimensions(shape: TLTextShape): {
|
getMinDimensions(shape: TLTextShape): {
|
||||||
height: number;
|
height: number;
|
||||||
|
@ -2489,7 +2486,7 @@ export type TLOnHandleChangeHandler<T extends TLShape> = (shape: T, info: {
|
||||||
export type TLOnResizeEndHandler<T extends TLShape> = TLEventChangeHandler<T>;
|
export type TLOnResizeEndHandler<T extends TLShape> = TLEventChangeHandler<T>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLOnResizeHandler<T extends TLShape> = (shape: T, info: TLResizeInfo<T>) => Partial<TLShapePartial<T>> | undefined | void;
|
export type TLOnResizeHandler<T extends TLShape> = (shape: T, info: TLResizeInfo<T>) => Omit<TLShapePartial<T>, 'id' | 'type'> | undefined | void;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLOnResizeStartHandler<T extends TLShape> = TLEventStartHandler<T>;
|
export type TLOnResizeStartHandler<T extends TLShape> = TLEventStartHandler<T>;
|
||||||
|
@ -2763,7 +2760,7 @@ export class VideoShapeUtil extends BaseBoxShapeUtil<TLVideoShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
component(shape: TLVideoShape): JSX.Element;
|
component(shape: TLVideoShape): JSX.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
defaultProps(): TLVideoShape['props'];
|
getDefaultProps(): TLVideoShape['props'];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
indicator(shape: TLVideoShape): JSX.Element;
|
indicator(shape: TLVideoShape): JSX.Element;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
// eslint-disable-next-line local/no-export-star
|
// eslint-disable-next-line local/no-export-star
|
||||||
export * from '@tldraw/indices'
|
export * from '@tldraw/indices'
|
||||||
|
export { defineMigrations } from '@tldraw/store'
|
||||||
// eslint-disable-next-line local/no-export-star
|
// eslint-disable-next-line local/no-export-star
|
||||||
export * from '@tldraw/tlschema'
|
export * from '@tldraw/tlschema'
|
||||||
export { getHashForString } from '@tldraw/utils'
|
export { getHashForString } from '@tldraw/utils'
|
||||||
|
|
|
@ -207,8 +207,7 @@ const HandlesWrapper = track(function HandlesWrapper() {
|
||||||
|
|
||||||
if (!(onlySelectedShape && shouldDisplayHandles)) return null
|
if (!(onlySelectedShape && shouldDisplayHandles)) return null
|
||||||
|
|
||||||
const util = editor.getShapeUtil(onlySelectedShape)
|
const handles = editor.getHandles(onlySelectedShape)
|
||||||
const handles = util.handles?.(onlySelectedShape)
|
|
||||||
|
|
||||||
if (!handles) return null
|
if (!handles) return null
|
||||||
|
|
||||||
|
|
|
@ -83,8 +83,7 @@ export const Shape = track(function Shape({
|
||||||
const shape = editor.getShapeById(id)
|
const shape = editor.getShapeById(id)
|
||||||
if (!shape) return null
|
if (!shape) return null
|
||||||
|
|
||||||
const util = editor.getShapeUtil(shape)
|
const bounds = editor.getBounds(shape)
|
||||||
const bounds = util.bounds(shape)
|
|
||||||
setProperty('width', Math.ceil(bounds.width) + 'px')
|
setProperty('width', Math.ceil(bounds.width) + 'px')
|
||||||
setProperty('height', Math.ceil(bounds.height) + 'px')
|
setProperty('height', Math.ceil(bounds.height) + 'px')
|
||||||
},
|
},
|
||||||
|
@ -106,7 +105,7 @@ export const Shape = track(function Shape({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{util.renderBackground && (
|
{util.backgroundComponent && (
|
||||||
<div
|
<div
|
||||||
ref={backgroundContainerRef}
|
ref={backgroundContainerRef}
|
||||||
className="tl-shape tl-shape-background"
|
className="tl-shape tl-shape-background"
|
||||||
|
@ -137,7 +136,7 @@ export const Shape = track(function Shape({
|
||||||
onPointerLeave={events.onPointerLeave}
|
onPointerLeave={events.onPointerLeave}
|
||||||
>
|
>
|
||||||
{isCulled && util.canUnmount(shape) ? (
|
{isCulled && util.canUnmount(shape) ? (
|
||||||
<CulledShape shape={shape} util={util} />
|
<CulledShape shape={shape} />
|
||||||
) : (
|
) : (
|
||||||
<OptionalErrorBoundary
|
<OptionalErrorBoundary
|
||||||
fallback={ShapeErrorFallback ? (error) => <ShapeErrorFallback error={error} /> : null}
|
fallback={ShapeErrorFallback ? (error) => <ShapeErrorFallback error={error} /> : null}
|
||||||
|
@ -168,14 +167,16 @@ const InnerShapeBackground = React.memo(
|
||||||
shape: T
|
shape: T
|
||||||
util: ShapeUtil<T>
|
util: ShapeUtil<T>
|
||||||
}) {
|
}) {
|
||||||
return useStateTracking('InnerShape:' + util.type, () => util.renderBackground?.(shape))
|
return useStateTracking('InnerShape:' + util.type, () => util.backgroundComponent?.(shape))
|
||||||
},
|
},
|
||||||
(prev, next) => prev.shape.props === next.shape.props
|
(prev, next) => prev.shape.props === next.shape.props
|
||||||
)
|
)
|
||||||
|
|
||||||
const CulledShape = React.memo(
|
const CulledShape = React.memo(
|
||||||
function CulledShap<T extends TLShape>({ shape, util }: { shape: T; util: ShapeUtil<T> }) {
|
function CulledShape<T extends TLShape>({ shape }: { shape: T }) {
|
||||||
const bounds = util.bounds(shape)
|
const editor = useEditor()
|
||||||
|
const bounds = editor.getBounds(shape)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="tl-shape__culled"
|
className="tl-shape__culled"
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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 =
|
const p =
|
||||||
point ?? (editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.viewportPageCenter)
|
point ?? (editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.viewportPageCenter)
|
||||||
|
|
||||||
const defaultProps = editor.getShapeUtil(TextShapeUtil).defaultProps()
|
const defaultProps = editor.getShapeUtil(TextShapeUtil).getDefaultProps()
|
||||||
|
|
||||||
const textToPaste = stripTrailingWhitespace(
|
const textToPaste = stripTrailingWhitespace(
|
||||||
stripCommonMinimumIndentation(replaceTabsWithSpaces(text))
|
stripCommonMinimumIndentation(replaceTabsWithSpaces(text))
|
||||||
|
|
|
@ -17,15 +17,15 @@ export abstract class BaseBoxShapeUtil<Shape extends TLBaseBoxShape> extends Sha
|
||||||
}
|
}
|
||||||
|
|
||||||
override getOutline(shape: Shape) {
|
override getOutline(shape: Shape) {
|
||||||
return this.bounds(shape).corners
|
return this.editor.getBounds(shape).corners
|
||||||
}
|
}
|
||||||
|
|
||||||
hitTestPoint(shape: Shape, point: VecLike): boolean {
|
override hitTestPoint(shape: Shape, point: VecLike): boolean {
|
||||||
return pointInPolygon(point, this.outline(shape))
|
return pointInPolygon(point, this.editor.getOutline(shape))
|
||||||
}
|
}
|
||||||
|
|
||||||
hitTestLineSegment(shape: Shape, A: VecLike, B: VecLike): boolean {
|
override hitTestLineSegment(shape: Shape, A: VecLike, B: VecLike): boolean {
|
||||||
const outline = this.outline(shape)
|
const outline = this.editor.getOutline(shape)
|
||||||
|
|
||||||
for (let i = 0; i < outline.length; i++) {
|
for (let i = 0; i < outline.length; i++) {
|
||||||
const C = outline[i]
|
const C = outline[i]
|
||||||
|
@ -36,7 +36,7 @@ export abstract class BaseBoxShapeUtil<Shape extends TLBaseBoxShape> extends Sha
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
onResize: TLOnResizeHandler<any> = (shape, info) => {
|
override onResize: TLOnResizeHandler<any> = (shape, info) => {
|
||||||
return resizeBox(shape, info)
|
return resizeBox(shape, info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { Box2d, linesIntersect, Vec2d, VecLike } from '@tldraw/primitives'
|
import { Box2d, linesIntersect, Vec2d, VecLike } from '@tldraw/primitives'
|
||||||
import { ComputedCache } from '@tldraw/store'
|
|
||||||
import { StyleProp, TLHandle, TLShape, TLShapePartial, TLUnknownShape } from '@tldraw/tlschema'
|
import { StyleProp, TLHandle, TLShape, TLShapePartial, TLUnknownShape } from '@tldraw/tlschema'
|
||||||
import { computed, EMPTY_ARRAY } from 'signia'
|
|
||||||
import type { Editor } from '../Editor'
|
import type { Editor } from '../Editor'
|
||||||
import { TLResizeHandle } from '../types/selection-types'
|
import { TLResizeHandle } from '../types/selection-types'
|
||||||
import { TLExportColors } from './shared/TLExportColors'
|
import { TLExportColors } from './shared/TLExportColors'
|
||||||
|
@ -67,6 +65,29 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
*/
|
*/
|
||||||
static type: string
|
static type: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default props for a shape.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
abstract getDefaultProps(): Shape['props']
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a JSX element for the shape (as an HTML element).
|
||||||
|
*
|
||||||
|
* @param shape - The shape.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
abstract component(shape: Shape): any
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get JSX describing the shape's indicator (as an SVG element).
|
||||||
|
*
|
||||||
|
* @param shape - The shape.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
abstract indicator(shape: Shape): any
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the shape can be snapped to by another shape.
|
* Whether the shape can be snapped to by another shape.
|
||||||
*
|
*
|
||||||
|
@ -118,14 +139,16 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
canCrop: TLShapeUtilFlag<Shape> = () => false
|
canCrop: TLShapeUtilFlag<Shape> = () => false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bounds of the shape to edit.
|
* Does this shape provide a background for its children? If this is true,
|
||||||
|
* then any children with a `renderBackground` method will have their
|
||||||
|
* backgrounds rendered _above_ this shape. Otherwise, the children's
|
||||||
|
* backgrounds will be rendered above either the next ancestor that provides
|
||||||
|
* a background, or the canvas background.
|
||||||
*
|
*
|
||||||
* Note: this could be a text area within a shape for example arrow labels.
|
* @internal
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
*/
|
||||||
getEditingBounds = (shape: Shape) => {
|
providesBackgroundForChildren(shape: Shape): boolean {
|
||||||
return this.bounds(shape)
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -170,36 +193,13 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
*/
|
*/
|
||||||
isAspectRatioLocked: TLShapeUtilFlag<Shape> = () => false
|
isAspectRatioLocked: TLShapeUtilFlag<Shape> = () => false
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the default props for a shape.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
abstract defaultProps(): Shape['props']
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a JSX element for the shape (as an HTML element).
|
|
||||||
*
|
|
||||||
* @param shape - The shape.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
abstract component(shape: Shape): any
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get JSX describing the shape's indicator (as an SVG element).
|
|
||||||
*
|
|
||||||
* @param shape - The shape.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
abstract indicator(shape: Shape): any
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a JSX element for the shape (as an HTML element) to be rendered as part of the canvas background - behind any other shape content.
|
* Get a JSX element for the shape (as an HTML element) to be rendered as part of the canvas background - behind any other shape content.
|
||||||
*
|
*
|
||||||
* @param shape - The shape.
|
* @param shape - The shape.
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
renderBackground?(shape: Shape): any
|
backgroundComponent?(shape: Shape): any
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an array of handle models for the shape. This is an optional method.
|
* Get an array of handle models for the shape. This is an optional method.
|
||||||
|
@ -213,25 +213,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
* @param shape - The shape.
|
* @param shape - The shape.
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
protected getHandles?(shape: Shape): TLHandle[]
|
getHandles?(shape: Shape): TLHandle[]
|
||||||
|
|
||||||
@computed
|
|
||||||
private get handlesCache(): ComputedCache<TLHandle[], TLShape> {
|
|
||||||
return this.editor.store.createComputedCache('handles:' + this.type, (shape) => {
|
|
||||||
return this.getHandles!(shape as any)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the cached handles (this should not be overridden!)
|
|
||||||
*
|
|
||||||
* @param shape - The shape.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
handles(shape: Shape): TLHandle[] {
|
|
||||||
if (!this.getHandles) return EMPTY_ARRAY
|
|
||||||
return this.handlesCache.get(shape.id) ?? EMPTY_ARRAY
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an array of outline segments for the shape. For most shapes,
|
* Get an array of outline segments for the shape. For most shapes,
|
||||||
|
@ -248,26 +230,8 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
* @param shape - The shape.
|
* @param shape - The shape.
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
protected getOutlineSegments(shape: Shape): Vec2d[][] {
|
getOutlineSegments(shape: Shape): Vec2d[][] {
|
||||||
return [this.outline(shape)]
|
return [this.editor.getOutline(shape)]
|
||||||
}
|
|
||||||
|
|
||||||
@computed
|
|
||||||
private get outlineSegmentsCache(): ComputedCache<Vec2d[][], TLShape> {
|
|
||||||
return this.editor.store.createComputedCache('outline-segments:' + this.type, (shape) => {
|
|
||||||
return this.getOutlineSegments!(shape as any)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the cached outline segments (this should not be overridden!)
|
|
||||||
*
|
|
||||||
* @param shape - The shape.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
outlineSegments(shape: Shape): Vec2d[][] {
|
|
||||||
if (!this.getOutlineSegments) return EMPTY_ARRAY
|
|
||||||
return this.outlineSegmentsCache.get(shape.id) ?? EMPTY_ARRAY
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -276,52 +240,16 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
* @param shape - The shape.
|
* @param shape - The shape.
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
protected abstract getBounds(shape: Shape): Box2d
|
abstract getBounds(shape: Shape): Box2d
|
||||||
|
|
||||||
@computed
|
|
||||||
private get boundsCache(): ComputedCache<Box2d, TLShape> {
|
|
||||||
return this.editor.store.createComputedCache('bounds:' + this.type, (shape) => {
|
|
||||||
return this.getBounds(shape as any)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the cached bounds for the shape.
|
* Get the shape's (not cached) outline.
|
||||||
*
|
*
|
||||||
* @param shape - The shape.
|
* @param shape - The shape.
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
bounds(shape: Shape): Box2d {
|
getOutline(shape: Shape): Vec2d[] {
|
||||||
const result = this.boundsCache.get(shape.id) ?? new Box2d()
|
return this.editor.getBounds(shape).corners
|
||||||
if (result.width === 0 || result.height === 0) {
|
|
||||||
return new Box2d(result.x, result.y, Math.max(result.width, 1), Math.max(result.height, 1))
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the shape's (not cached) outline. Do not override this method!
|
|
||||||
*
|
|
||||||
* @param shape - The shape.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
protected abstract getOutline(shape: Shape): Vec2d[]
|
|
||||||
|
|
||||||
@computed
|
|
||||||
private get outlineCache(): ComputedCache<Vec2d[], TLShape> {
|
|
||||||
return this.editor.store.createComputedCache('outline:' + this.type, (shape) => {
|
|
||||||
return this.getOutline(shape as any)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the shape's outline. Do not override this method!
|
|
||||||
*
|
|
||||||
* @param shape - The shape.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
outline(shape: Shape): Vec2d[] {
|
|
||||||
return this.outlineCache.get(shape.id) ?? EMPTY_ARRAY
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -331,7 +259,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
snapPoints(shape: Shape) {
|
snapPoints(shape: Shape) {
|
||||||
return this.bounds(shape).snapPoints
|
return this.editor.getBounds(shape).snapPoints
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -350,7 +278,9 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
* @param shape - The shape.
|
* @param shape - The shape.
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
abstract getCenter(shape: Shape): Vec2d
|
getCenter(shape: Shape) {
|
||||||
|
return this.editor.getBounds(shape).center
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get whether the shape can receive children of a given type.
|
* Get whether the shape can receive children of a given type.
|
||||||
|
@ -403,6 +333,11 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
colors: TLExportColors
|
colors: TLExportColors
|
||||||
): SVGElement | Promise<SVGElement> | null
|
): SVGElement | Promise<SVGElement> | null
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
expandSelectionOutlinePx(shape: Shape): number {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get whether a point intersects the shape.
|
* Get whether a point intersects the shape.
|
||||||
*
|
*
|
||||||
|
@ -412,7 +347,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
hitTestPoint(shape: Shape, point: VecLike): boolean {
|
hitTestPoint(shape: Shape, point: VecLike): boolean {
|
||||||
return this.bounds(shape).containsPoint(point)
|
return this.editor.getBounds(shape).containsPoint(point)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -425,7 +360,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
hitTestLineSegment(shape: Shape, A: VecLike, B: VecLike): boolean {
|
hitTestLineSegment(shape: Shape, A: VecLike, B: VecLike): boolean {
|
||||||
const outline = this.outline(shape)
|
const outline = this.editor.getOutline(shape)
|
||||||
|
|
||||||
for (let i = 0; i < outline.length; i++) {
|
for (let i = 0; i < outline.length; i++) {
|
||||||
const C = outline[i]
|
const C = outline[i]
|
||||||
|
@ -436,24 +371,6 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
expandSelectionOutlinePx(shape: Shape): number {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Does this shape provide a background for its children? If this is true,
|
|
||||||
* then any children with a `renderBackground` method will have their
|
|
||||||
* backgrounds rendered _above_ this shape. Otherwise, the children's
|
|
||||||
* backgrounds will be rendered above either the next ancestor that provides
|
|
||||||
* a background, or the canvas background.
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
providesBackgroundForChildren(shape: Shape): boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -745,7 +662,7 @@ export type TLResizeInfo<T extends TLShape> = {
|
||||||
export type TLOnResizeHandler<T extends TLShape> = (
|
export type TLOnResizeHandler<T extends TLShape> = (
|
||||||
shape: T,
|
shape: T,
|
||||||
info: TLResizeInfo<T>
|
info: TLResizeInfo<T>
|
||||||
) => Partial<TLShapePartial<T>> | undefined | void
|
) => Omit<TLShapePartial<T>, 'id' | 'type'> | undefined | void
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLOnResizeStartHandler<T extends TLShape> = TLEventStartHandler<T>
|
export type TLOnResizeStartHandler<T extends TLShape> = TLEventStartHandler<T>
|
||||||
|
|
|
@ -73,7 +73,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
override hideSelectionBoundsFg: TLShapeUtilFlag<TLArrowShape> = () => true
|
override hideSelectionBoundsFg: TLShapeUtilFlag<TLArrowShape> = () => true
|
||||||
override hideSelectionBoundsBg: TLShapeUtilFlag<TLArrowShape> = () => true
|
override hideSelectionBoundsBg: TLShapeUtilFlag<TLArrowShape> = () => true
|
||||||
|
|
||||||
override defaultProps(): TLArrowShape['props'] {
|
override getDefaultProps(): TLArrowShape['props'] {
|
||||||
return {
|
return {
|
||||||
dash: 'draw',
|
dash: 'draw',
|
||||||
size: 'm',
|
size: 'm',
|
||||||
|
@ -91,7 +91,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
getCenter(shape: TLArrowShape): Vec2d {
|
getCenter(shape: TLArrowShape): Vec2d {
|
||||||
return this.bounds(shape).center
|
return this.editor.getBounds(shape).center
|
||||||
}
|
}
|
||||||
|
|
||||||
getBounds(shape: TLArrowShape) {
|
getBounds(shape: TLArrowShape) {
|
||||||
|
@ -292,7 +292,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
|
|
||||||
if (util.isClosed(hitShape)) {
|
if (util.isClosed(hitShape)) {
|
||||||
// Test the polygon
|
// Test the polygon
|
||||||
return pointInPolygon(pointInTargetSpace, util.outline(hitShape))
|
return pointInPolygon(pointInTargetSpace, this.editor.getOutline(hitShape))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test the point using the shape's idea of what a hit is
|
// Test the point using the shape's idea of what a hit is
|
||||||
|
@ -533,7 +533,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
hitTestPoint(shape: TLArrowShape, point: VecLike): boolean {
|
hitTestPoint(shape: TLArrowShape, point: VecLike): boolean {
|
||||||
const outline = this.outline(shape)
|
const outline = this.editor.getOutline(shape)
|
||||||
const zoomLevel = this.editor.zoomLevel
|
const zoomLevel = this.editor.zoomLevel
|
||||||
const offsetDist = STROKE_SIZES[shape.props.size] / zoomLevel
|
const offsetDist = STROKE_SIZES[shape.props.size] / zoomLevel
|
||||||
|
|
||||||
|
@ -548,7 +548,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
hitTestLineSegment(shape: TLArrowShape, A: VecLike, B: VecLike): boolean {
|
hitTestLineSegment(shape: TLArrowShape, A: VecLike, B: VecLike): boolean {
|
||||||
const outline = this.outline(shape)
|
const outline = this.editor.getOutline(shape)
|
||||||
|
|
||||||
for (let i = 0; i < outline.length - 1; i++) {
|
for (let i = 0; i < outline.length - 1; i++) {
|
||||||
const C = outline[i]
|
const C = outline[i]
|
||||||
|
@ -571,7 +571,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
) && !this.editor.isReadOnly
|
) && !this.editor.isReadOnly
|
||||||
|
|
||||||
const info = this.getArrowInfo(shape)
|
const info = this.getArrowInfo(shape)
|
||||||
const bounds = this.bounds(shape)
|
const bounds = this.editor.getBounds(shape)
|
||||||
const labelSize = this.getLabelBounds(shape)
|
const labelSize = this.getLabelBounds(shape)
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
@ -750,7 +750,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
const { start, end } = getArrowTerminalsInArrowSpace(this.editor, shape)
|
const { start, end } = getArrowTerminalsInArrowSpace(this.editor, shape)
|
||||||
|
|
||||||
const info = this.getArrowInfo(shape)
|
const info = this.getArrowInfo(shape)
|
||||||
const bounds = this.bounds(shape)
|
const bounds = this.editor.getBounds(shape)
|
||||||
const labelSize = this.getLabelBounds(shape)
|
const labelSize = this.getLabelBounds(shape)
|
||||||
|
|
||||||
if (!info) return null
|
if (!info) return null
|
||||||
|
@ -844,7 +844,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
@computed get labelBoundsCache(): ComputedCache<Box2d | null, TLArrowShape> {
|
@computed get labelBoundsCache(): ComputedCache<Box2d | null, TLArrowShape> {
|
||||||
return this.editor.store.createComputedCache('labelBoundsCache', (shape) => {
|
return this.editor.store.createComputedCache('labelBoundsCache', (shape) => {
|
||||||
const info = this.getArrowInfo(shape)
|
const info = this.getArrowInfo(shape)
|
||||||
const bounds = this.bounds(shape)
|
const bounds = this.editor.getBounds(shape)
|
||||||
const { text, font, size } = shape.props
|
const { text, font, size } = shape.props
|
||||||
|
|
||||||
if (!info) return null
|
if (!info) return null
|
||||||
|
@ -901,10 +901,6 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
return this.labelBoundsCache.get(shape.id) || null
|
return this.labelBoundsCache.get(shape.id) || null
|
||||||
}
|
}
|
||||||
|
|
||||||
getEditingBounds = (shape: TLArrowShape): Box2d => {
|
|
||||||
return this.getLabelBounds(shape) ?? new Box2d()
|
|
||||||
}
|
|
||||||
|
|
||||||
onEditEnd: TLOnEditEndHandler<TLArrowShape> = (shape) => {
|
onEditEnd: TLOnEditEndHandler<TLArrowShape> = (shape) => {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
|
@ -941,7 +937,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
// Arrowhead end path
|
// Arrowhead end path
|
||||||
const ae = info.end.arrowhead && getArrowheadPathForType(info, 'end', strokeWidth)
|
const ae = info.end.arrowhead && getArrowheadPathForType(info, 'end', strokeWidth)
|
||||||
|
|
||||||
const bounds = this.bounds(shape)
|
const bounds = this.editor.getBounds(shape)
|
||||||
const labelSize = this.getLabelBounds(shape)
|
const labelSize = this.getLabelBounds(shape)
|
||||||
|
|
||||||
const maskId = (shape.id + '_clip').replace(':', '_')
|
const maskId = (shape.id + '_clip').replace(':', '_')
|
||||||
|
|
|
@ -74,7 +74,7 @@ export function getCurvedArrowInfo(editor: Editor, shape: TLArrowShape, extraBen
|
||||||
const endInStartShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, endInPageSpace)
|
const endInStartShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, endInPageSpace)
|
||||||
const centerInStartShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, centerInPageSpace)
|
const centerInStartShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, centerInPageSpace)
|
||||||
|
|
||||||
const isClosed = startShapeInfo.util.isClosed(startShapeInfo.shape)
|
const { isClosed } = startShapeInfo
|
||||||
const fn = isClosed ? intersectCirclePolygon : intersectCirclePolyline
|
const fn = isClosed ? intersectCirclePolygon : intersectCirclePolyline
|
||||||
|
|
||||||
let point: VecLike | undefined
|
let point: VecLike | undefined
|
||||||
|
@ -82,7 +82,7 @@ export function getCurvedArrowInfo(editor: Editor, shape: TLArrowShape, extraBen
|
||||||
let intersections = fn(
|
let intersections = fn(
|
||||||
centerInStartShapeLocalSpace,
|
centerInStartShapeLocalSpace,
|
||||||
handleArc.radius,
|
handleArc.radius,
|
||||||
startShapeInfo.util.outline(startShapeInfo.shape)
|
editor.getOutline(startShapeInfo.shape)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (intersections) {
|
if (intersections) {
|
||||||
|
@ -150,7 +150,7 @@ export function getCurvedArrowInfo(editor: Editor, shape: TLArrowShape, extraBen
|
||||||
const endInEndShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, endInPageSpace)
|
const endInEndShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, endInPageSpace)
|
||||||
const centerInEndShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, centerInPageSpace)
|
const centerInEndShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, centerInPageSpace)
|
||||||
|
|
||||||
const isClosed = endShapeInfo.util.isClosed(endShapeInfo.shape)
|
const isClosed = endShapeInfo.isClosed
|
||||||
const fn = isClosed ? intersectCirclePolygon : intersectCirclePolyline
|
const fn = isClosed ? intersectCirclePolygon : intersectCirclePolyline
|
||||||
|
|
||||||
const angleToMiddle = Vec2d.Angle(handleArc.center, middle)
|
const angleToMiddle = Vec2d.Angle(handleArc.center, middle)
|
||||||
|
@ -162,7 +162,7 @@ export function getCurvedArrowInfo(editor: Editor, shape: TLArrowShape, extraBen
|
||||||
let intersections = fn(
|
let intersections = fn(
|
||||||
centerInEndShapeLocalSpace,
|
centerInEndShapeLocalSpace,
|
||||||
handleArc.radius,
|
handleArc.radius,
|
||||||
endShapeInfo.util.outline(endShapeInfo.shape)
|
editor.getOutline(endShapeInfo.shape)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (intersections) {
|
if (intersections) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Matrix2d, Vec2d } from '@tldraw/primitives'
|
import { Matrix2d, Vec2d } from '@tldraw/primitives'
|
||||||
import { TLArrowShape, TLArrowShapeTerminal, TLShape } from '@tldraw/tlschema'
|
import { TLArrowShape, TLArrowShapeTerminal, TLShape } from '@tldraw/tlschema'
|
||||||
import { Editor } from '../../../Editor'
|
import { Editor } from '../../../Editor'
|
||||||
import { ShapeUtil } from '../../ShapeUtil'
|
|
||||||
|
|
||||||
export function getIsArrowStraight(shape: TLArrowShape) {
|
export function getIsArrowStraight(shape: TLArrowShape) {
|
||||||
return Math.abs(shape.props.bend) < 8 // snap to +-8px
|
return Math.abs(shape.props.bend) < 8 // snap to +-8px
|
||||||
|
@ -9,10 +8,11 @@ export function getIsArrowStraight(shape: TLArrowShape) {
|
||||||
|
|
||||||
export type BoundShapeInfo<T extends TLShape = TLShape> = {
|
export type BoundShapeInfo<T extends TLShape = TLShape> = {
|
||||||
shape: T
|
shape: T
|
||||||
util: ShapeUtil<T>
|
|
||||||
didIntersect: boolean
|
didIntersect: boolean
|
||||||
isExact: boolean
|
isExact: boolean
|
||||||
|
isClosed: boolean
|
||||||
transform: Matrix2d
|
transform: Matrix2d
|
||||||
|
outline: Vec2d[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBoundShapeInfoForTerminal(
|
export function getBoundShapeInfoForTerminal(
|
||||||
|
@ -29,10 +29,11 @@ export function getBoundShapeInfoForTerminal(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
shape,
|
shape,
|
||||||
util,
|
|
||||||
transform,
|
transform,
|
||||||
|
isClosed: util.isClosed(shape),
|
||||||
isExact: terminal.isExact,
|
isExact: terminal.isExact,
|
||||||
didIntersect: false,
|
didIntersect: false,
|
||||||
|
outline: editor.getOutline(shape),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -67,14 +67,14 @@ export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): Arrow
|
||||||
if (endShapeInfo.didIntersect && !startShapeInfo.didIntersect) {
|
if (endShapeInfo.didIntersect && !startShapeInfo.didIntersect) {
|
||||||
// ...and if only the end shape intersected, then make it
|
// ...and if only the end shape intersected, then make it
|
||||||
// a short arrow ending at the end shape intersection.
|
// a short arrow ending at the end shape intersection.
|
||||||
if (startShapeInfo.util.isClosed(startShapeInfo.shape)) {
|
if (startShapeInfo.isClosed) {
|
||||||
a.setTo(Vec2d.Nudge(b, a, minDist))
|
a.setTo(Vec2d.Nudge(b, a, minDist))
|
||||||
}
|
}
|
||||||
} else if (!endShapeInfo.didIntersect) {
|
} else if (!endShapeInfo.didIntersect) {
|
||||||
// ...and if only the end shape intersected, or if neither
|
// ...and if only the end shape intersected, or if neither
|
||||||
// shape intersected, then make it a short arrow starting
|
// shape intersected, then make it a short arrow starting
|
||||||
// at the start shape intersection.
|
// at the start shape intersection.
|
||||||
if (endShapeInfo.util.isClosed(endShapeInfo.shape)) {
|
if (endShapeInfo.isClosed) {
|
||||||
b.setTo(Vec2d.Nudge(a, b, minDist))
|
b.setTo(Vec2d.Nudge(a, b, minDist))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,10 +179,10 @@ function updateArrowheadPointWithBoundShape(
|
||||||
const targetFrom = Matrix2d.applyToPoint(Matrix2d.Inverse(targetShapeInfo.transform), pageFrom)
|
const targetFrom = Matrix2d.applyToPoint(Matrix2d.Inverse(targetShapeInfo.transform), pageFrom)
|
||||||
const targetTo = Matrix2d.applyToPoint(Matrix2d.Inverse(targetShapeInfo.transform), pageTo)
|
const targetTo = Matrix2d.applyToPoint(Matrix2d.Inverse(targetShapeInfo.transform), pageTo)
|
||||||
|
|
||||||
const isClosed = targetShapeInfo.util.isClosed(targetShapeInfo.shape)
|
const isClosed = targetShapeInfo.isClosed
|
||||||
const fn = isClosed ? intersectLineSegmentPolygon : intersectLineSegmentPolyline
|
const fn = isClosed ? intersectLineSegmentPolygon : intersectLineSegmentPolyline
|
||||||
|
|
||||||
const intersection = fn(targetFrom, targetTo, targetShapeInfo.util.outline(targetShapeInfo.shape))
|
const intersection = fn(targetFrom, targetTo, targetShapeInfo.outline)
|
||||||
|
|
||||||
let targetInt: VecLike | undefined
|
let targetInt: VecLike | undefined
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ export class Pointing extends StateNode {
|
||||||
const shape = this.editor.getShapeById<TLArrowShape>(id)
|
const shape = this.editor.getShapeById<TLArrowShape>(id)
|
||||||
if (!shape) return
|
if (!shape) return
|
||||||
|
|
||||||
const handles = util.handles?.(shape)
|
const handles = this.editor.getHandles(shape)
|
||||||
|
|
||||||
if (handles) {
|
if (handles) {
|
||||||
// start precise
|
// start precise
|
||||||
|
@ -82,8 +82,7 @@ export class Pointing extends StateNode {
|
||||||
if (!this.shape) return
|
if (!this.shape) return
|
||||||
|
|
||||||
if (this.editor.inputs.isDragging) {
|
if (this.editor.inputs.isDragging) {
|
||||||
const util = this.editor.getShapeUtil(this.shape)
|
const handles = this.editor.getHandles(this.shape)
|
||||||
const handles = util.handles?.(this.shape)
|
|
||||||
|
|
||||||
if (!handles) {
|
if (!handles) {
|
||||||
this.editor.bailToMark('creating')
|
this.editor.bailToMark('creating')
|
||||||
|
@ -96,8 +95,6 @@ export class Pointing extends StateNode {
|
||||||
|
|
||||||
if (!shape) return
|
if (!shape) return
|
||||||
|
|
||||||
const handles = util.handles(shape)
|
|
||||||
|
|
||||||
if (handles) {
|
if (handles) {
|
||||||
const { x, y } = this.editor.getPointInShapeSpace(
|
const { x, y } = this.editor.getPointInShapeSpace(
|
||||||
shape,
|
shape,
|
||||||
|
|
|
@ -23,7 +23,7 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
|
||||||
override hideSelectionBoundsBg = () => true
|
override hideSelectionBoundsBg = () => true
|
||||||
override hideSelectionBoundsFg = () => true
|
override hideSelectionBoundsFg = () => true
|
||||||
|
|
||||||
override defaultProps(): TLBookmarkShape['props'] {
|
override getDefaultProps(): TLBookmarkShape['props'] {
|
||||||
return {
|
return {
|
||||||
url: '',
|
url: '',
|
||||||
w: 300,
|
w: 300,
|
||||||
|
|
|
@ -30,7 +30,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
||||||
hideSelectionBoundsBg = (shape: TLDrawShape) => getIsDot(shape)
|
hideSelectionBoundsBg = (shape: TLDrawShape) => getIsDot(shape)
|
||||||
hideSelectionBoundsFg = (shape: TLDrawShape) => getIsDot(shape)
|
hideSelectionBoundsFg = (shape: TLDrawShape) => getIsDot(shape)
|
||||||
|
|
||||||
override defaultProps(): TLDrawShape['props'] {
|
override getDefaultProps(): TLDrawShape['props'] {
|
||||||
return {
|
return {
|
||||||
segments: [],
|
segments: [],
|
||||||
color: 'black',
|
color: 'black',
|
||||||
|
@ -46,7 +46,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
||||||
isClosed = (shape: TLDrawShape) => shape.props.isClosed
|
isClosed = (shape: TLDrawShape) => shape.props.isClosed
|
||||||
|
|
||||||
getBounds(shape: TLDrawShape) {
|
getBounds(shape: TLDrawShape) {
|
||||||
return Box2d.FromPoints(this.outline(shape))
|
return Box2d.FromPoints(this.editor.getOutline(shape))
|
||||||
}
|
}
|
||||||
|
|
||||||
getOutline(shape: TLDrawShape) {
|
getOutline(shape: TLDrawShape) {
|
||||||
|
@ -54,11 +54,11 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
getCenter(shape: TLDrawShape): Vec2d {
|
getCenter(shape: TLDrawShape): Vec2d {
|
||||||
return this.bounds(shape).center
|
return this.editor.getBounds(shape).center
|
||||||
}
|
}
|
||||||
|
|
||||||
hitTestPoint(shape: TLDrawShape, point: VecLike): boolean {
|
hitTestPoint(shape: TLDrawShape, point: VecLike): boolean {
|
||||||
const outline = this.outline(shape)
|
const outline = this.editor.getOutline(shape)
|
||||||
const zoomLevel = this.editor.zoomLevel
|
const zoomLevel = this.editor.zoomLevel
|
||||||
const offsetDist = STROKE_SIZES[shape.props.size] / zoomLevel
|
const offsetDist = STROKE_SIZES[shape.props.size] / zoomLevel
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
||||||
return pointInPolygon(point, outline)
|
return pointInPolygon(point, outline)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.bounds(shape).containsPoint(point)) {
|
if (this.editor.getBounds(shape).containsPoint(point)) {
|
||||||
for (let i = 0; i < outline.length; i++) {
|
for (let i = 0; i < outline.length; i++) {
|
||||||
const C = outline[i]
|
const C = outline[i]
|
||||||
const D = outline[(i + 1) % outline.length]
|
const D = outline[(i + 1) % outline.length]
|
||||||
|
@ -85,7 +85,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
hitTestLineSegment(shape: TLDrawShape, A: VecLike, B: VecLike): boolean {
|
hitTestLineSegment(shape: TLDrawShape, A: VecLike, B: VecLike): boolean {
|
||||||
const outline = this.outline(shape)
|
const outline = this.editor.getOutline(shape)
|
||||||
|
|
||||||
if (shape.props.segments.length === 1 && shape.props.segments[0].points.length < 4) {
|
if (shape.props.segments.length === 1 && shape.props.segments[0].points.length < 4) {
|
||||||
const zoomLevel = this.editor.zoomLevel
|
const zoomLevel = this.editor.zoomLevel
|
||||||
|
|
|
@ -38,7 +38,7 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
|
||||||
return !!getEmbedInfo(shape.props.url)?.definition?.doesResize
|
return !!getEmbedInfo(shape.props.url)?.definition?.doesResize
|
||||||
}
|
}
|
||||||
|
|
||||||
override defaultProps(): TLEmbedShape['props'] {
|
override getDefaultProps(): TLEmbedShape['props'] {
|
||||||
return {
|
return {
|
||||||
w: 300,
|
w: 300,
|
||||||
h: 300,
|
h: 300,
|
||||||
|
|
|
@ -18,12 +18,12 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
||||||
|
|
||||||
override canEdit = () => true
|
override canEdit = () => true
|
||||||
|
|
||||||
override defaultProps(): TLFrameShape['props'] {
|
override getDefaultProps(): TLFrameShape['props'] {
|
||||||
return { w: 160 * 2, h: 90 * 2, name: '' }
|
return { w: 160 * 2, h: 90 * 2, name: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
override component(shape: TLFrameShape) {
|
override component(shape: TLFrameShape) {
|
||||||
const bounds = this.bounds(shape)
|
const bounds = this.editor.getBounds(shape)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -137,7 +137,7 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
indicator(shape: TLFrameShape) {
|
indicator(shape: TLFrameShape) {
|
||||||
const bounds = this.bounds(shape)
|
const bounds = this.editor.getBounds(shape)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<rect
|
<rect
|
||||||
|
|
|
@ -50,7 +50,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
|
|
||||||
canEdit = () => true
|
canEdit = () => true
|
||||||
|
|
||||||
override defaultProps(): TLGeoShape['props'] {
|
override getDefaultProps(): TLGeoShape['props'] {
|
||||||
return {
|
return {
|
||||||
w: 100,
|
w: 100,
|
||||||
h: 100,
|
h: 100,
|
||||||
|
@ -70,7 +70,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
hitTestLineSegment(shape: TLGeoShape, A: VecLike, B: VecLike): boolean {
|
hitTestLineSegment(shape: TLGeoShape, A: VecLike, B: VecLike): boolean {
|
||||||
const outline = this.outline(shape)
|
const outline = this.editor.getOutline(shape)
|
||||||
|
|
||||||
// Check the outline
|
// Check the outline
|
||||||
for (let i = 0; i < outline.length; i++) {
|
for (let i = 0; i < outline.length; i++) {
|
||||||
|
@ -91,7 +91,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
hitTestPoint(shape: TLGeoShape, point: VecLike): boolean {
|
hitTestPoint(shape: TLGeoShape, point: VecLike): boolean {
|
||||||
const outline = this.outline(shape)
|
const outline = this.editor.getOutline(shape)
|
||||||
|
|
||||||
if (shape.props.fill === 'none') {
|
if (shape.props.fill === 'none') {
|
||||||
const zoomLevel = this.editor.zoomLevel
|
const zoomLevel = this.editor.zoomLevel
|
||||||
|
@ -397,7 +397,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
const outline = this.outline(shape)
|
const outline = this.editor.getOutline(shape)
|
||||||
const lines = getLines(shape.props, strokeWidth)
|
const lines = getLines(shape.props, strokeWidth)
|
||||||
|
|
||||||
if (dash === 'solid' || (dash === 'draw' && forceSolid)) {
|
if (dash === 'solid' || (dash === 'draw' && forceSolid)) {
|
||||||
|
@ -479,7 +479,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
const outline = this.outline(shape)
|
const outline = this.editor.getOutline(shape)
|
||||||
let path: string
|
let path: string
|
||||||
|
|
||||||
if (props.dash === 'draw' && !forceSolid) {
|
if (props.dash === 'draw' && !forceSolid) {
|
||||||
|
@ -591,7 +591,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
const outline = this.outline(shape)
|
const outline = this.editor.getOutline(shape)
|
||||||
const lines = getLines(shape.props, strokeWidth)
|
const lines = getLines(shape.props, strokeWidth)
|
||||||
|
|
||||||
switch (props.dash) {
|
switch (props.dash) {
|
||||||
|
@ -635,7 +635,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.text) {
|
if (props.text) {
|
||||||
const bounds = this.bounds(shape)
|
const bounds = this.editor.getBounds(shape)
|
||||||
|
|
||||||
const rootTextElm = getTextLabelSvgElement({
|
const rootTextElm = getTextLabelSvgElement({
|
||||||
editor: this.editor,
|
editor: this.editor,
|
||||||
|
|
|
@ -15,7 +15,7 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
|
||||||
|
|
||||||
canBind = () => false
|
canBind = () => false
|
||||||
|
|
||||||
defaultProps(): TLGroupShape['props'] {
|
getDefaultProps(): TLGroupShape['props'] {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,11 +36,11 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
getCenter(shape: TLGroupShape): Vec2d {
|
getCenter(shape: TLGroupShape): Vec2d {
|
||||||
return this.bounds(shape).center
|
return this.editor.getBounds(shape).center
|
||||||
}
|
}
|
||||||
|
|
||||||
getOutline(shape: TLGroupShape): Vec2d[] {
|
getOutline(shape: TLGroupShape): Vec2d[] {
|
||||||
return this.bounds(shape).corners
|
return this.editor.getBounds(shape).corners
|
||||||
}
|
}
|
||||||
|
|
||||||
component(shape: TLGroupShape) {
|
component(shape: TLGroupShape) {
|
||||||
|
@ -71,7 +71,7 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const bounds = this.bounds(shape)
|
const bounds = this.editor.getBounds(shape)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SVGContainer id={shape.id}>
|
<SVGContainer id={shape.id}>
|
||||||
|
@ -86,7 +86,7 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
|
||||||
camera: { z: zoomLevel },
|
camera: { z: zoomLevel },
|
||||||
} = this.editor
|
} = this.editor
|
||||||
|
|
||||||
const bounds = this.bounds(shape)
|
const bounds = this.editor.getBounds(shape)
|
||||||
|
|
||||||
return <DashedOutlineBox className="" bounds={bounds} zoomLevel={zoomLevel} />
|
return <DashedOutlineBox className="" bounds={bounds} zoomLevel={zoomLevel} />
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
||||||
hideSelectionBoundsBg = (shape: TLHighlightShape) => getIsDot(shape)
|
hideSelectionBoundsBg = (shape: TLHighlightShape) => getIsDot(shape)
|
||||||
hideSelectionBoundsFg = (shape: TLHighlightShape) => getIsDot(shape)
|
hideSelectionBoundsFg = (shape: TLHighlightShape) => getIsDot(shape)
|
||||||
|
|
||||||
override defaultProps(): TLHighlightShape['props'] {
|
override getDefaultProps(): TLHighlightShape['props'] {
|
||||||
return {
|
return {
|
||||||
segments: [],
|
segments: [],
|
||||||
color: 'black',
|
color: 'black',
|
||||||
|
@ -33,7 +33,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
getBounds(shape: TLHighlightShape) {
|
getBounds(shape: TLHighlightShape) {
|
||||||
return Box2d.FromPoints(this.outline(shape))
|
return Box2d.FromPoints(this.editor.getOutline(shape))
|
||||||
}
|
}
|
||||||
|
|
||||||
getOutline(shape: TLHighlightShape) {
|
getOutline(shape: TLHighlightShape) {
|
||||||
|
@ -41,11 +41,11 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
getCenter(shape: TLHighlightShape): Vec2d {
|
getCenter(shape: TLHighlightShape): Vec2d {
|
||||||
return this.bounds(shape).center
|
return this.editor.getBounds(shape).center
|
||||||
}
|
}
|
||||||
|
|
||||||
hitTestPoint(shape: TLHighlightShape, point: VecLike): boolean {
|
hitTestPoint(shape: TLHighlightShape, point: VecLike): boolean {
|
||||||
const outline = this.outline(shape)
|
const outline = this.editor.getOutline(shape)
|
||||||
const zoomLevel = this.editor.zoomLevel
|
const zoomLevel = this.editor.zoomLevel
|
||||||
const offsetDist = getStrokeWidth(shape) / zoomLevel
|
const offsetDist = getStrokeWidth(shape) / zoomLevel
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.bounds(shape).containsPoint(point)) {
|
if (this.editor.getBounds(shape).containsPoint(point)) {
|
||||||
for (let i = 0; i < outline.length; i++) {
|
for (let i = 0; i < outline.length; i++) {
|
||||||
const C = outline[i]
|
const C = outline[i]
|
||||||
const D = outline[(i + 1) % outline.length]
|
const D = outline[(i + 1) % outline.length]
|
||||||
|
@ -68,7 +68,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
hitTestLineSegment(shape: TLHighlightShape, A: VecLike, B: VecLike): boolean {
|
hitTestLineSegment(shape: TLHighlightShape, A: VecLike, B: VecLike): boolean {
|
||||||
const outline = this.outline(shape)
|
const outline = this.editor.getOutline(shape)
|
||||||
|
|
||||||
if (shape.props.segments.length === 1 && shape.props.segments[0].points.length < 4) {
|
if (shape.props.segments.length === 1 && shape.props.segments[0].points.length < 4) {
|
||||||
const zoomLevel = this.editor.zoomLevel
|
const zoomLevel = this.editor.zoomLevel
|
||||||
|
@ -102,7 +102,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderBackground(shape: TLHighlightShape) {
|
backgroundComponent(shape: TLHighlightShape) {
|
||||||
return (
|
return (
|
||||||
<HighlightRenderer
|
<HighlightRenderer
|
||||||
strokeWidth={getStrokeWidth(shape)}
|
strokeWidth={getStrokeWidth(shape)}
|
||||||
|
|
|
@ -54,7 +54,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
||||||
override isAspectRatioLocked = () => true
|
override isAspectRatioLocked = () => true
|
||||||
override canCrop = () => true
|
override canCrop = () => true
|
||||||
|
|
||||||
override defaultProps(): TLImageShape['props'] {
|
override getDefaultProps(): TLImageShape['props'] {
|
||||||
return {
|
return {
|
||||||
w: 100,
|
w: 100,
|
||||||
h: 100,
|
h: 100,
|
||||||
|
|
|
@ -35,7 +35,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
||||||
override hideSelectionBoundsFg = () => true
|
override hideSelectionBoundsFg = () => true
|
||||||
override isClosed = () => false
|
override isClosed = () => false
|
||||||
|
|
||||||
override defaultProps(): TLLineShape['props'] {
|
override getDefaultProps(): TLLineShape['props'] {
|
||||||
return {
|
return {
|
||||||
dash: 'draw',
|
dash: 'draw',
|
||||||
size: 'm',
|
size: 'm',
|
||||||
|
@ -68,10 +68,6 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
||||||
return spline.bounds
|
return spline.bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
getCenter(shape: TLLineShape) {
|
|
||||||
return this.bounds(shape).center
|
|
||||||
}
|
|
||||||
|
|
||||||
getHandles(shape: TLLineShape) {
|
getHandles(shape: TLLineShape) {
|
||||||
return handlesCache.get(shape.props, () => {
|
return handlesCache.get(shape.props, () => {
|
||||||
const handles = shape.props.handles
|
const handles = shape.props.handles
|
||||||
|
@ -174,11 +170,11 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
||||||
hitTestPoint(shape: TLLineShape, point: Vec2d): boolean {
|
hitTestPoint(shape: TLLineShape, point: Vec2d): boolean {
|
||||||
const zoomLevel = this.editor.zoomLevel
|
const zoomLevel = this.editor.zoomLevel
|
||||||
const offsetDist = STROKE_SIZES[shape.props.size] / zoomLevel
|
const offsetDist = STROKE_SIZES[shape.props.size] / zoomLevel
|
||||||
return pointNearToPolyline(point, this.outline(shape), offsetDist)
|
return pointNearToPolyline(point, this.editor.getOutline(shape), offsetDist)
|
||||||
}
|
}
|
||||||
|
|
||||||
hitTestLineSegment(shape: TLLineShape, A: VecLike, B: VecLike): boolean {
|
hitTestLineSegment(shape: TLLineShape, A: VecLike, B: VecLike): boolean {
|
||||||
return intersectLineSegmentPolyline(A, B, this.outline(shape)) !== null
|
return intersectLineSegmentPolyline(A, B, this.editor.getOutline(shape)) !== null
|
||||||
}
|
}
|
||||||
|
|
||||||
component(shape: TLLineShape) {
|
component(shape: TLLineShape) {
|
||||||
|
|
|
@ -29,7 +29,8 @@ export class Pointing extends StateNode {
|
||||||
|
|
||||||
// if user is holding shift then we are adding points to an existing line
|
// if user is holding shift then we are adding points to an existing line
|
||||||
if (inputs.shiftKey && shapeExists) {
|
if (inputs.shiftKey && shapeExists) {
|
||||||
const handles = this.editor.getShapeUtil(this.shape).handles(this.shape)
|
const handles = this.editor.getHandles(this.shape)
|
||||||
|
if (!handles) return
|
||||||
|
|
||||||
const vertexHandles = handles.filter((h) => h.type === 'vertex').sort(sortByIndex)
|
const vertexHandles = handles.filter((h) => h.type === 'vertex').sort(sortByIndex)
|
||||||
const endHandle = vertexHandles[vertexHandles.length - 1]
|
const endHandle = vertexHandles[vertexHandles.length - 1]
|
||||||
|
@ -96,8 +97,7 @@ export class Pointing extends StateNode {
|
||||||
if (!this.shape) return
|
if (!this.shape) return
|
||||||
|
|
||||||
if (this.editor.inputs.isDragging) {
|
if (this.editor.inputs.isDragging) {
|
||||||
const util = this.editor.getShapeUtil(this.shape)
|
const handles = this.editor.getHandles(this.shape)
|
||||||
const handles = util.handles?.(this.shape)
|
|
||||||
if (!handles) {
|
if (!handles) {
|
||||||
this.editor.bailToMark('creating')
|
this.editor.bailToMark('creating')
|
||||||
throw Error('No handles found')
|
throw Error('No handles found')
|
||||||
|
|
|
@ -19,7 +19,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||||
hideSelectionBoundsBg = () => true
|
hideSelectionBoundsBg = () => true
|
||||||
hideSelectionBoundsFg = () => true
|
hideSelectionBoundsFg = () => true
|
||||||
|
|
||||||
defaultProps(): TLNoteShape['props'] {
|
getDefaultProps(): TLNoteShape['props'] {
|
||||||
return {
|
return {
|
||||||
color: 'black',
|
color: 'black',
|
||||||
size: 'm',
|
size: 'm',
|
||||||
|
@ -42,7 +42,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
getOutline(shape: TLNoteShape) {
|
getOutline(shape: TLNoteShape) {
|
||||||
return this.bounds(shape).corners
|
return this.editor.getBounds(shape).corners
|
||||||
}
|
}
|
||||||
|
|
||||||
getCenter(_shape: TLNoteShape) {
|
getCenter(_shape: TLNoteShape) {
|
||||||
|
@ -106,7 +106,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
toSvg(shape: TLNoteShape, font: string, colors: TLExportColors) {
|
toSvg(shape: TLNoteShape, font: string, colors: TLExportColors) {
|
||||||
const bounds = this.bounds(shape)
|
const bounds = this.getBounds(shape)
|
||||||
|
|
||||||
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { TLNoteShape, createShapeId } from '@tldraw/tlschema'
|
import { TLNoteShape, createShapeId } from '@tldraw/tlschema'
|
||||||
import { NoteShapeUtil } from '../../../shapes/note/NoteShapeUtil'
|
|
||||||
import { StateNode } from '../../../tools/StateNode'
|
import { StateNode } from '../../../tools/StateNode'
|
||||||
import { TLEventHandlers, TLInterruptEvent, TLPointerEventInfo } from '../../../types/event-types'
|
import { TLEventHandlers, TLInterruptEvent, TLPointerEventInfo } from '../../../types/event-types'
|
||||||
|
|
||||||
|
@ -97,9 +96,8 @@ export class Pointing extends StateNode {
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
const util = this.editor.getShapeUtil(NoteShapeUtil)
|
|
||||||
const shape = this.editor.getShapeById<TLNoteShape>(id)!
|
const shape = this.editor.getShapeById<TLNoteShape>(id)!
|
||||||
const bounds = util.bounds(shape)
|
const bounds = this.editor.getBounds(shape)
|
||||||
|
|
||||||
// Center the text around the created point
|
// Center the text around the created point
|
||||||
this.editor.updateShapes([
|
this.editor.updateShapes([
|
||||||
|
|
|
@ -24,7 +24,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
||||||
|
|
||||||
isAspectRatioLocked: TLShapeUtilFlag<TLTextShape> = () => true
|
isAspectRatioLocked: TLShapeUtilFlag<TLTextShape> = () => true
|
||||||
|
|
||||||
defaultProps(): TLTextShape['props'] {
|
getDefaultProps(): TLTextShape['props'] {
|
||||||
return {
|
return {
|
||||||
color: 'black',
|
color: 'black',
|
||||||
size: 'm',
|
size: 'm',
|
||||||
|
@ -63,7 +63,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
getOutline(shape: TLTextShape) {
|
getOutline(shape: TLTextShape) {
|
||||||
const bounds = this.bounds(shape)
|
const bounds = this.editor.getBounds(shape)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
new Vec2d(0, 0),
|
new Vec2d(0, 0),
|
||||||
|
@ -73,11 +73,6 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
getCenter(shape: TLTextShape): Vec2d {
|
|
||||||
const bounds = this.bounds(shape)
|
|
||||||
return new Vec2d(bounds.width / 2, bounds.height / 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
component(shape: TLTextShape) {
|
component(shape: TLTextShape) {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
|
@ -150,12 +145,12 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
indicator(shape: TLTextShape) {
|
indicator(shape: TLTextShape) {
|
||||||
const bounds = this.bounds(shape)
|
const bounds = this.getBounds(shape)
|
||||||
return <rect width={toDomPrecision(bounds.width)} height={toDomPrecision(bounds.height)} />
|
return <rect width={toDomPrecision(bounds.width)} height={toDomPrecision(bounds.height)} />
|
||||||
}
|
}
|
||||||
|
|
||||||
toSvg(shape: TLTextShape, font: string | undefined, colors: TLExportColors) {
|
toSvg(shape: TLTextShape, font: string | undefined, colors: TLExportColors) {
|
||||||
const bounds = this.bounds(shape)
|
const bounds = this.getBounds(shape)
|
||||||
const text = shape.props.text
|
const text = shape.props.text
|
||||||
|
|
||||||
const width = bounds.width / (shape.props.scale ?? 1)
|
const width = bounds.width / (shape.props.scale ?? 1)
|
||||||
|
@ -204,7 +199,11 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
||||||
const { initialBounds, initialShape, scaleX, handle } = info
|
const { initialBounds, initialShape, scaleX, handle } = info
|
||||||
|
|
||||||
if (info.mode === 'scale_shape' || (handle !== 'right' && handle !== 'left')) {
|
if (info.mode === 'scale_shape' || (handle !== 'right' && handle !== 'left')) {
|
||||||
return resizeScaled(shape, info)
|
return {
|
||||||
|
id: shape.id,
|
||||||
|
type: shape.type,
|
||||||
|
...resizeScaled(shape, info),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const prevWidth = initialBounds.width
|
const prevWidth = initialBounds.width
|
||||||
let nextWidth = prevWidth * scaleX
|
let nextWidth = prevWidth * scaleX
|
||||||
|
@ -227,6 +226,8 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
||||||
const { x, y } = offset.rot(shape.rotation).add(initialShape)
|
const { x, y } = offset.rot(shape.rotation).add(initialShape)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
id: shape.id,
|
||||||
|
type: shape.type,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
props: {
|
props: {
|
||||||
|
|
|
@ -16,7 +16,7 @@ export class VideoShapeUtil extends BaseBoxShapeUtil<TLVideoShape> {
|
||||||
override canEdit = () => true
|
override canEdit = () => true
|
||||||
override isAspectRatioLocked = () => true
|
override isAspectRatioLocked = () => true
|
||||||
|
|
||||||
override defaultProps(): TLVideoShape['props'] {
|
override getDefaultProps(): TLVideoShape['props'] {
|
||||||
return {
|
return {
|
||||||
w: 100,
|
w: 100,
|
||||||
h: 100,
|
h: 100,
|
||||||
|
|
|
@ -94,7 +94,7 @@ export class Pointing extends StateNode {
|
||||||
])
|
])
|
||||||
|
|
||||||
const shape = this.editor.getShapeById<TLBaseBoxShape>(id)!
|
const shape = this.editor.getShapeById<TLBaseBoxShape>(id)!
|
||||||
const { w, h } = this.editor.getShapeUtil(shape).defaultProps() as TLBaseBoxShape['props']
|
const { w, h } = this.editor.getShapeUtil(shape).getDefaultProps() as TLBaseBoxShape['props']
|
||||||
const delta = this.editor.getDeltaInParentSpace(shape, new Vec2d(w / 2, h / 2))
|
const delta = this.editor.getDeltaInParentSpace(shape, new Vec2d(w / 2, h / 2))
|
||||||
|
|
||||||
this.editor.updateShapes<TLBaseBoxShape>([
|
this.editor.updateShapes<TLBaseBoxShape>([
|
||||||
|
|
|
@ -5,6 +5,7 @@ let editor: TestEditor
|
||||||
|
|
||||||
const ids = {
|
const ids = {
|
||||||
box1: createShapeId('box1'),
|
box1: createShapeId('box1'),
|
||||||
|
line1: createShapeId('line1'),
|
||||||
embed1: createShapeId('embed1'),
|
embed1: createShapeId('embed1'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +136,8 @@ describe('PointingHandle', () => {
|
||||||
|
|
||||||
describe('DraggingHandle', () => {
|
describe('DraggingHandle', () => {
|
||||||
it('Enters from pointing_handle and exits to idle', () => {
|
it('Enters from pointing_handle and exits to idle', () => {
|
||||||
const shape = editor.getShapeById(ids.box1)
|
editor.createShapes([{ id: ids.line1, type: 'line', x: 100, y: 100 }])
|
||||||
|
const shape = editor.getShapeById(ids.line1)
|
||||||
editor.pointerDown(150, 150, {
|
editor.pointerDown(150, 150, {
|
||||||
target: 'handle',
|
target: 'handle',
|
||||||
shape,
|
shape,
|
||||||
|
@ -149,7 +151,8 @@ describe('DraggingHandle', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Bails on escape', () => {
|
it('Bails on escape', () => {
|
||||||
const shape = editor.getShapeById(ids.box1)
|
editor.createShapes([{ id: ids.line1, type: 'line', x: 100, y: 100 }])
|
||||||
|
const shape = editor.getShapeById(ids.line1)
|
||||||
|
|
||||||
editor.pointerDown(150, 150, {
|
editor.pointerDown(150, 150, {
|
||||||
target: 'handle',
|
target: 'handle',
|
||||||
|
|
|
@ -58,7 +58,7 @@ export class DraggingHandle extends StateNode {
|
||||||
this.editor.setCursor({ type: isCreating ? 'cross' : 'grabbing', rotation: 0 })
|
this.editor.setCursor({ type: isCreating ? 'cross' : 'grabbing', rotation: 0 })
|
||||||
|
|
||||||
// <!-- Only relevant to arrows
|
// <!-- Only relevant to arrows
|
||||||
const handles = this.editor.getShapeUtil(shape).handles(shape).sort(sortByIndex)
|
const handles = this.editor.getHandles(shape)!.sort(sortByIndex)
|
||||||
const index = handles.findIndex((h) => h.id === info.handle.id)
|
const index = handles.findIndex((h) => h.id === info.handle.id)
|
||||||
|
|
||||||
// Find the adjacent handle
|
// Find the adjacent handle
|
||||||
|
@ -227,14 +227,14 @@ export class DraggingHandle extends StateNode {
|
||||||
|
|
||||||
// Get all the outline segments from the shape
|
// Get all the outline segments from the shape
|
||||||
const additionalSegments = util
|
const additionalSegments = util
|
||||||
.outlineSegments(shape)
|
.getOutlineSegments(shape)
|
||||||
.map((segment) => Matrix2d.applyToPoints(pageTransform, segment))
|
.map((segment) => Matrix2d.applyToPoints(pageTransform, segment))
|
||||||
|
|
||||||
// We want to skip the segments that include the handle, so
|
// We want to skip the segments that include the handle, so
|
||||||
// find the index of the handle that shares the same index property
|
// find the index of the handle that shares the same index property
|
||||||
// as the initial dragging handle; this catches a quirk of create handles
|
// as the initial dragging handle; this catches a quirk of create handles
|
||||||
const handleIndex = util
|
const handleIndex = editor
|
||||||
.handles(shape)
|
.getHandles(shape)!
|
||||||
.filter(({ type }) => type === 'vertex')
|
.filter(({ type }) => type === 'vertex')
|
||||||
.sort(sortByIndex)
|
.sort(sortByIndex)
|
||||||
.findIndex(({ index }) => initialHandle.index === index)
|
.findIndex(({ index }) => initialHandle.index === index)
|
||||||
|
|
|
@ -405,7 +405,7 @@ export class Resizing extends StateNode {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
shape,
|
shape,
|
||||||
bounds: util.bounds(shape),
|
bounds: this.editor.getBounds(shape),
|
||||||
pageTransform,
|
pageTransform,
|
||||||
pageRotation: Matrix2d.Decompose(pageTransform!).rotation,
|
pageRotation: Matrix2d.Decompose(pageTransform!).rotation,
|
||||||
isAspectRatioLocked: util.isAspectRatioLocked(shape),
|
isAspectRatioLocked: util.isAspectRatioLocked(shape),
|
||||||
|
|
|
@ -11,6 +11,21 @@ export function useDocumentEvents() {
|
||||||
|
|
||||||
const isAppFocused = useValue('isFocused', () => editor.isFocused, [editor])
|
const isAppFocused = useValue('isFocused', () => editor.isFocused, [editor])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof matchMedia !== undefined) return
|
||||||
|
|
||||||
|
function updateDevicePixelRatio() {
|
||||||
|
editor.setDevicePixelRatio(window.devicePixelRatio)
|
||||||
|
}
|
||||||
|
|
||||||
|
const MM = matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
|
||||||
|
|
||||||
|
MM.addEventListener('change', updateDevicePixelRatio)
|
||||||
|
return () => {
|
||||||
|
MM.removeEventListener('change', updateDevicePixelRatio)
|
||||||
|
}
|
||||||
|
}, [editor])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAppFocused) return
|
if (!isAppFocused) return
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,7 @@ import { useEditor } from './useEditor'
|
||||||
|
|
||||||
function getHandle(editor: Editor, id: TLShapeId, handleId: string) {
|
function getHandle(editor: Editor, id: TLShapeId, handleId: string) {
|
||||||
const shape = editor.getShapeById<TLArrowShape | TLLineShape>(id)!
|
const shape = editor.getShapeById<TLArrowShape | TLLineShape>(id)!
|
||||||
const util = editor.getShapeUtil(shape)
|
const handles = editor.getHandles(shape)!
|
||||||
const handles = util.handles(shape)
|
|
||||||
return { shape, handle: handles.find((h) => h.id === handleId) }
|
return { shape, handle: handles.find((h) => h.id === handleId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -455,7 +455,7 @@ describe('getShapeUtil', () => {
|
||||||
class MyFakeShapeUtil extends BaseBoxShapeUtil<any> {
|
class MyFakeShapeUtil extends BaseBoxShapeUtil<any> {
|
||||||
static type = 'fake'
|
static type = 'fake'
|
||||||
|
|
||||||
defaultProps() {
|
getDefaultProps() {
|
||||||
throw new Error('Method not implemented.')
|
throw new Error('Method not implemented.')
|
||||||
}
|
}
|
||||||
component() {
|
component() {
|
||||||
|
@ -475,7 +475,7 @@ describe('getShapeUtil', () => {
|
||||||
class MyFakeGeoShapeUtil extends BaseBoxShapeUtil<any> {
|
class MyFakeGeoShapeUtil extends BaseBoxShapeUtil<any> {
|
||||||
static type = 'geo'
|
static type = 'geo'
|
||||||
|
|
||||||
defaultProps() {
|
getDefaultProps() {
|
||||||
throw new Error('Method not implemented.')
|
throw new Error('Method not implemented.')
|
||||||
}
|
}
|
||||||
component() {
|
component() {
|
||||||
|
|
|
@ -275,7 +275,7 @@ describe('Custom shapes', () => {
|
||||||
override canResize = (_shape: CardShape) => true
|
override canResize = (_shape: CardShape) => true
|
||||||
override canBind = (_shape: CardShape) => true
|
override canBind = (_shape: CardShape) => true
|
||||||
|
|
||||||
override defaultProps(): CardShape['props'] {
|
override getDefaultProps(): CardShape['props'] {
|
||||||
return {
|
return {
|
||||||
w: 300,
|
w: 300,
|
||||||
h: 300,
|
h: 300,
|
||||||
|
@ -283,7 +283,7 @@ describe('Custom shapes', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
component(shape: CardShape) {
|
component(shape: CardShape) {
|
||||||
const bounds = this.bounds(shape)
|
const bounds = this.getBounds(shape)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HTMLContainer
|
<HTMLContainer
|
||||||
|
|
|
@ -33,7 +33,7 @@ describe('creating frames', () => {
|
||||||
editor.setSelectedTool('frame')
|
editor.setSelectedTool('frame')
|
||||||
editor.pointerDown(100, 100).pointerUp(100, 100)
|
editor.pointerDown(100, 100).pointerUp(100, 100)
|
||||||
expect(editor.onlySelectedShape?.type).toBe('frame')
|
expect(editor.onlySelectedShape?.type).toBe('frame')
|
||||||
const { w, h } = editor.getShapeUtil(FrameShapeUtil).defaultProps()
|
const { w, h } = editor.getShapeUtil(FrameShapeUtil).getDefaultProps()
|
||||||
expect(editor.getPageBounds(editor.onlySelectedShape!)).toMatchObject({
|
expect(editor.getPageBounds(editor.onlySelectedShape!)).toMatchObject({
|
||||||
x: 100 - w / 2,
|
x: 100 - w / 2,
|
||||||
y: 100 - h / 2,
|
y: 100 - h / 2,
|
||||||
|
|
|
@ -11,7 +11,6 @@ import {
|
||||||
} from '@tldraw/tlschema'
|
} from '@tldraw/tlschema'
|
||||||
import { assert, compact } from '@tldraw/utils'
|
import { assert, compact } from '@tldraw/utils'
|
||||||
import { ArrowShapeTool } from '../../editor/shapes/arrow/ArrowShapeTool'
|
import { ArrowShapeTool } from '../../editor/shapes/arrow/ArrowShapeTool'
|
||||||
import { ArrowShapeUtil } from '../../editor/shapes/arrow/ArrowShapeUtil'
|
|
||||||
import { DrawShapeTool } from '../../editor/shapes/draw/DrawShapeTool'
|
import { DrawShapeTool } from '../../editor/shapes/draw/DrawShapeTool'
|
||||||
import { GroupShapeUtil } from '../../editor/shapes/group/GroupShapeUtil'
|
import { GroupShapeUtil } from '../../editor/shapes/group/GroupShapeUtil'
|
||||||
import { LineShapeTool } from '../../editor/shapes/line/LineShapeTool'
|
import { LineShapeTool } from '../../editor/shapes/line/LineShapeTool'
|
||||||
|
@ -1679,10 +1678,7 @@ describe('moving handles within a group', () => {
|
||||||
editor.pointerDown(60, 60, {
|
editor.pointerDown(60, 60, {
|
||||||
target: 'handle',
|
target: 'handle',
|
||||||
shape: arrow,
|
shape: arrow,
|
||||||
handle: editor
|
handle: editor.getHandles<TLArrowShape>(arrow)!.find((h) => h.id === 'end'),
|
||||||
.getShapeUtil(ArrowShapeUtil)
|
|
||||||
.handles(arrow)
|
|
||||||
.find((h) => h.id === 'end'),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
editor.expectToBeIn('select.pointing_handle')
|
editor.expectToBeIn('select.pointing_handle')
|
||||||
|
|
|
@ -14,7 +14,7 @@ type __TopLeftSnapOnlyShape = any
|
||||||
class __TopLeftSnapOnlyShapeUtil extends ShapeUtil<__TopLeftSnapOnlyShape> {
|
class __TopLeftSnapOnlyShapeUtil extends ShapeUtil<__TopLeftSnapOnlyShape> {
|
||||||
static override type = '__test_top_left_snap_only' as const
|
static override type = '__test_top_left_snap_only' as const
|
||||||
|
|
||||||
defaultProps(): __TopLeftSnapOnlyShape['props'] {
|
getDefaultProps(): __TopLeftSnapOnlyShape['props'] {
|
||||||
return { width: 10, height: 10 }
|
return { width: 10, height: 10 }
|
||||||
}
|
}
|
||||||
getBounds(shape: __TopLeftSnapOnlyShape): Box2d {
|
getBounds(shape: __TopLeftSnapOnlyShape): Box2d {
|
||||||
|
|
|
@ -550,7 +550,7 @@ export function buildFromV1Document(editor: Editor, document: LegacyTldrawDocume
|
||||||
y: bounds.minY + bounds.height * ny,
|
y: bounds.minY + bounds.height * ny,
|
||||||
})
|
})
|
||||||
|
|
||||||
const handles = util.handles(v2ShapeFresh)
|
const handles = editor.getHandles(v2ShapeFresh)!
|
||||||
|
|
||||||
const change = util.onHandleChange!(v2ShapeFresh, {
|
const change = util.onHandleChange!(v2ShapeFresh, {
|
||||||
handle: {
|
handle: {
|
||||||
|
|
|
@ -12,6 +12,7 @@ export function Tldraw(props: TldrawEditorProps & TldrawUiProps): JSX.Element;
|
||||||
|
|
||||||
|
|
||||||
export * from "@tldraw/editor";
|
export * from "@tldraw/editor";
|
||||||
|
export * from "@tldraw/primitives";
|
||||||
export * from "@tldraw/ui";
|
export * from "@tldraw/ui";
|
||||||
|
|
||||||
// (No @packageDocumentation comment for this package)
|
// (No @packageDocumentation comment for this package)
|
||||||
|
|
|
@ -46,6 +46,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tldraw/editor": "workspace:*",
|
"@tldraw/editor": "workspace:*",
|
||||||
"@tldraw/polyfills": "workspace:*",
|
"@tldraw/polyfills": "workspace:*",
|
||||||
|
"@tldraw/primitives": "workspace:*",
|
||||||
|
"@tldraw/store": "workspace:*",
|
||||||
"@tldraw/ui": "workspace:*"
|
"@tldraw/ui": "workspace:*"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|
|
@ -2,9 +2,10 @@
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
import '@tldraw/polyfills'
|
import '@tldraw/polyfills'
|
||||||
|
|
||||||
// eslint-disable-next-line local/no-export-star
|
// eslint-disable-next-line local/no-export-star
|
||||||
export * from '@tldraw/editor'
|
export * from '@tldraw/editor'
|
||||||
// eslint-disable-next-line local/no-export-star
|
// eslint-disable-next-line local/no-export-star
|
||||||
|
export * from '@tldraw/primitives'
|
||||||
|
// eslint-disable-next-line local/no-export-star
|
||||||
export * from '@tldraw/ui'
|
export * from '@tldraw/ui'
|
||||||
export { Tldraw } from './lib/Tldraw'
|
export { Tldraw } from './lib/Tldraw'
|
||||||
|
|
|
@ -706,7 +706,7 @@ export type ShapeProps<Shape extends TLBaseShape<any, any>> = {
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export class StyleProp<Type> implements T.Validatable<Type> {
|
export class StyleProp<Type> implements T.Validatable<Type> {
|
||||||
protected constructor(id: string, defaultValue: Type, type: T.Validatable<Type>);
|
constructor(id: string, defaultValue: Type, type: T.Validatable<Type>);
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
readonly defaultValue: Type;
|
readonly defaultValue: Type;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
|
|
@ -16,7 +16,7 @@ export class StyleProp<Type> implements T.Validatable<Type> {
|
||||||
return new EnumStyleProp<Values[number]>(uniqueId, defaultValue, values)
|
return new EnumStyleProp<Values[number]>(uniqueId, defaultValue, values)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected constructor(
|
constructor(
|
||||||
readonly id: string,
|
readonly id: string,
|
||||||
readonly defaultValue: Type,
|
readonly defaultValue: Type,
|
||||||
readonly type: T.Validatable<Type>
|
readonly type: T.Validatable<Type>
|
||||||
|
|
|
@ -4626,6 +4626,8 @@ __metadata:
|
||||||
"@testing-library/react": ^14.0.0
|
"@testing-library/react": ^14.0.0
|
||||||
"@tldraw/editor": "workspace:*"
|
"@tldraw/editor": "workspace:*"
|
||||||
"@tldraw/polyfills": "workspace:*"
|
"@tldraw/polyfills": "workspace:*"
|
||||||
|
"@tldraw/primitives": "workspace:*"
|
||||||
|
"@tldraw/store": "workspace:*"
|
||||||
"@tldraw/ui": "workspace:*"
|
"@tldraw/ui": "workspace:*"
|
||||||
chokidar-cli: ^3.0.0
|
chokidar-cli: ^3.0.0
|
||||||
jest-canvas-mock: ^2.4.0
|
jest-canvas-mock: ^2.4.0
|
||||||
|
|
Loading…
Reference in a new issue