ShapeUtil refactor, Editor cleanup (#1611)

This PR improves the ergonomics of `ShapeUtil` classes.

### Cached methods

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

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

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

### More defaults

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

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

### Editing bounds

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

### Active area manager

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

### Dpr manager

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

### Moving things around

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

### Change Type

- [x] `major` — Breaking change

### Release Notes

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

View file

@ -11,6 +11,126 @@ keywords:
- utils
---
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