4f70a4f4e8
Describe what your pull request does. If appropriate, add GIFs or images showing the before and after. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `galaxy brain` — Architectural changes ### Test Plan 1. Add a step-by-step description of how to test your PR here. 2. - [ ] Unit Tests - [ ] End to end tests ### Release Notes #### BREAKING CHANGES - The `Migrations` type is now called `LegacyMigrations`. - The serialized schema format (e.g. returned by `StoreSchema.serialize()` and `Store.getSnapshot()`) has changed. You don't need to do anything about it unless you were reading data directly from the schema for some reason. In which case it'd be best to avoid that in the future! We have no plans to change the schema format again (this time was traumatic enough) but you never know. - `compareRecordVersions` and the `RecordVersion` type have both disappeared. There is no replacement. These were public by mistake anyway, so hopefully nobody had been using it. - `compareSchemas` is a bit less useful now. Our migrations system has become a little fuzzy to allow for simpler UX when adding/removing custom extensions and 3rd party dependencies, and as a result we can no longer compare serialized schemas in any rigorous manner. You can rely on this function to return `0` if the schemas are the same. Otherwise it will return `-1` if the schema on the right _seems_ to be newer than the schema on the left, but it cannot guarantee that in situations where migration sequences have been removed over time (e.g. if you remove one of the builtin tldraw shapes). Generally speaking, the best way to check schema compatibility now is to call `store.schema.getMigrationsSince(persistedSchema)`. This will throw an error if there is no upgrade path from the `persistedSchema` to the current version. - `defineMigrations` has been deprecated and will be removed in a future release. For upgrade instructions see https://tldraw.dev/docs/persistence#Updating-legacy-shape-migrations-defineMigrations - `migrate` has been removed. Nobody should have been using this but if you were you'll need to find an alternative. For migrating tldraw data, you should stick to using `schema.migrateStoreSnapshot` and, if you are building a nuanced sync engine that supports some amount of backwards compatibility, also feel free to use `schema.migratePersistedRecord`. - the `Migration` type has changed. If you need the old one for some reason it has been renamed to `LegacyMigration`. It will be removed in a future release. - the `Migrations` type has been renamed to `LegacyMigrations` and will be removed in a future release. - the `SerializedSchema` type has been augmented. If you need the old version specifically you can use `SerializedSchemaV1` --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
244 lines
8.2 KiB
Text
244 lines
8.2 KiB
Text
---
|
|
title: Shapes
|
|
status: published
|
|
author: steveruizok
|
|
date: 3/22/2023
|
|
order: 2
|
|
keywords:
|
|
- custom
|
|
- shapes
|
|
- shapeutils
|
|
- utils
|
|
---
|
|
|
|
In tldraw, a shape is something that can exist on the page, like an arrow, an image, or some text.
|
|
|
|
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.
|
|
|
|
## Types of shape
|
|
|
|
We make a distinction between three types of shapes: "core", "default", and "custom".
|
|
|
|
### Core shapes
|
|
|
|
The editor's core shapes are shapes that are built in and always present. At the moment the only core shape is the [group shape](/reference/tlschema/TLGroupShape).
|
|
|
|
### Default shapes
|
|
|
|
The default shapes are all of the shapes that are included by default in the [Tldraw](?) component, such as the [TLArrowShape](?) or [TLDrawShape](?). They are exported from the `tldraw` library as [defaultShapeUtils](?).
|
|
|
|
### Custom shapes
|
|
|
|
Custom shapes are shapes that were created by you or someone you love. Find more information about custom shapes [below](#Custom-shapes-1).
|
|
|
|
## The shape object
|
|
|
|
Shapes are just records (JSON objects) that sit in the [store](/docs/editor#Store). For example, here's a shape record for a rectangle geo shape:
|
|
|
|
```ts
|
|
{
|
|
"parentId": "page:somePage",
|
|
"id": "shape:someId",
|
|
"typeName": "shape"
|
|
"type": "geo",
|
|
"x": 106,
|
|
"y": 294,
|
|
"rotation": 0,
|
|
"index": "a28",
|
|
"opacity": 1,
|
|
"isLocked": false,
|
|
"props": {
|
|
"w": 200,
|
|
"h": 200,
|
|
"geo": "rectangle",
|
|
"color": "black",
|
|
"labelColor": "black",
|
|
"fill": "none",
|
|
"dash": "draw",
|
|
"size": "m",
|
|
"font": "draw",
|
|
"text": "diagram",
|
|
"align": "middle",
|
|
"verticalAlign": "middle",
|
|
"growY": 0,
|
|
"url": ""
|
|
},
|
|
"meta": {},
|
|
}
|
|
```
|
|
|
|
### Base properties
|
|
|
|
Every shape contains some base information. These include the shape's type, position, rotation, opacity, and more. You can find the full list of base properties [here](/reference/tlschema/TLBaseShape).
|
|
|
|
### Props
|
|
|
|
Every shape also contains some shape-specific information, called `props`. Each type of shape can have different props. For example, the `props` of a text shape are much different than the props of an arrow shape.
|
|
|
|
### Meta
|
|
|
|
Meta information is information that is not used by tldraw but is instead used by your application. For example, you might want to store the name of the user who created a shape, or the date that the shape was created. You can find more information about meta information [below](#Meta-information).
|
|
|
|
## The `ShapeUtil` class
|
|
|
|
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 render a text shape, it will find the [TextShapeUtil](?) and call its [ShapeUtil#component](?) method, passing in the text shape object as an argument.
|
|
|
|
---
|
|
|
|
## Custom shapes
|
|
|
|
You can create your own custom shapes. In the examples below, we will create a custom "card" shape. It'll be a simple rectangle with some text inside.
|
|
|
|
> For an example of how to create custom shapes, see our [custom shapes example](/examples/shapes/tools/custom-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'
|
|
|
|
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 base properties of a shape, such as `x`, `y`, `rotation`, and `opacity`.
|
|
|
|
### Shape Util
|
|
|
|
While tldraw's shapes themselves are simple JSON objects, we use [ShapeUtil](?) classes to answer questions about shapes.
|
|
|
|
Let's create a [ShapeUtil](?) class for the shape.
|
|
|
|
```tsx
|
|
import { HTMLContainer, ShapeUtil } from 'tldraw'
|
|
|
|
class CardShapeUtil extends ShapeUtil<CardShape> {
|
|
static override type = 'card' as const
|
|
|
|
getDefaultProps(): CardShape['props'] {
|
|
return {
|
|
w: 100,
|
|
h: 100,
|
|
}
|
|
}
|
|
|
|
getGeometry(shape: ICardShape) {
|
|
return new Rectangle2d({
|
|
width: shape.props.w,
|
|
height: shape.props.h,
|
|
isFilled: true,
|
|
})
|
|
}
|
|
|
|
component(shape: CardShape) {
|
|
return <HTMLContainer>Hello</HTMLContainer>
|
|
}
|
|
|
|
indicator(shape: CardShape) {
|
|
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 [ShapeUtil#getDefaultProps](?), [ShapeUtil#getBounds](?), [ShapeUtil#component](?), and [ShapeUtil#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.
|
|
|
|
### The `shapeUtils` prop
|
|
|
|
We pass an array of our shape utils into the [Tldraw](?) component's `shapeUtils` prop.
|
|
|
|
```tsx
|
|
const MyCustomShapes = [MyCardShape]
|
|
|
|
export default function () {
|
|
return (
|
|
<div style={{ position: 'fixed', inset: 0 }}>
|
|
<Tldraw shapeUtils={MyCustomShapes} />
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
We can create one of our custom card shapes using the [Editor](?) API. We'll do this by setting the `onMount` prop of the [Tldraw](?) component.
|
|
|
|
```tsx
|
|
export default function () {
|
|
return (
|
|
<div style={{ position: 'fixed', inset: 0 }}>
|
|
<Tldraw
|
|
shapeUtils={MyCustomShapes}
|
|
onMount={(editor) => {
|
|
editor.createShapes([{ type: 'card' }])
|
|
}}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
Once the page refreshes, we should now have our custom shape on the canvas.
|
|
|
|
### Meta information
|
|
|
|
Shapes also have a `meta` property (see [TLBaseShape#meta](?)) that you can fill with your own data. This should feel like a bit of a hack, however it's intended to be an escape hatch for applications where you want to use tldraw's existing shapes but also want to attach a bit of extra data to the shape.
|
|
|
|
Note that tldraw's regular shape definitions have an unknown object for the shape's `meta` property. To type your shape's meta, use a union like this:
|
|
|
|
```ts
|
|
type MyShapeWithMeta = TLGeoShape & { meta: { createdBy: string } }
|
|
|
|
const shape = editor.getShape<MyShapeWithMeta>(myGeoShape.id)
|
|
```
|
|
|
|
You can update a shape's `meta` property in the same way you would update its props, using [Editor#updateShapes](?).
|
|
|
|
```ts
|
|
editor.updateShapes<MyShapeWithMeta>([
|
|
{
|
|
id: myGeoShape.id,
|
|
type: 'geo',
|
|
meta: {
|
|
createdBy: 'Steve',
|
|
},
|
|
},
|
|
])
|
|
```
|
|
|
|
Like [TLBaseShape#props](?), the data in a [TLBaseShape#meta](?) object must be JSON serializable.
|
|
|
|
In addition to setting meta properties this way, you can also set the default meta data for shapes using the Editor's [Editor#getInitialMetaForShape](?) method.
|
|
|
|
```tsx
|
|
editor.getInitialMetaForShape = (shape: TLShape) => {
|
|
if (shape.type === 'text') {
|
|
return { createdBy: currentUser.id, lastModified: Date.now() }
|
|
} else {
|
|
return { createdBy: currentUser.id }
|
|
}
|
|
}
|
|
```
|
|
|
|
Whenever new shapes are created using the [Editor#createShapes](?) method, the shape's meta property will be set using the [Editor#getInitialMetaForShape](?) method. By default this method returns an empty object.
|
|
|
|
### Using starter shapes
|
|
|
|
You can use "starter" shape utils like [BaseBoxShapeUtil](?) to get regular rectangular shape behavior.
|
|
|
|
### Flags
|
|
|
|
You can use flags like [ShapeUtil#hideRotateHandle](?) to hide different parts of the UI when the shape is selected, or else to control different behaviors of the shape.
|
|
|
|
### Interaction
|
|
|
|
You can turn on `pointer-events` to allow users to interact inside of the shape.
|
|
|
|
### Editing
|
|
|
|
You can make shapes "editable" to help decide when they're interactive or not.
|
|
|
|
### Migrations
|
|
|
|
You can add migrations for your shape props by adding a `migrations` property to your shape's util class. See [the persistence docs](/docs/persistence#Shape-props-migrations) for more information.
|