Fix and annotate minimal example (#2448)

The minimal example code made reference to a delete functionality that
wasn't working. I managed to get it working again and updated the
background component accordingly. I'm not sure of the reason to check
for a hovered shape before checking if you hit a shape, so I took that
part out.

There was also a check to see if the target was a 'selection' that
didn't seem to ever fire, so I removed that as the selection behaviour
worked fine.

The main thing is the annotations though.

### Change Type

- [ ] `patch` — Bug fix
- [ ] `minor` — New feature
- [ ] `major` — Breaking change
- [ ] `dependencies` — Changes to package dependencies[^1]
- [x] `documentation` — Changes to the documentation only[^2]
- [ ] `tests` — Changes to any test code only[^2]
- [ ] `internal` — Any other changes that don't affect the published
package[^2]
- [ ] I don't know

[^1]: publishes a `patch` release, for devDependencies use `internal`
[^2]: will not publish a new version

### 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

- Fix and annotate minimal example
This commit is contained in:
Taha 2024-01-12 15:52:28 +00:00 committed by GitHub
parent b4d343d784
commit 6c5dd85feb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 195 additions and 72 deletions

View file

@ -1,31 +1,17 @@
import { StateNode, TLEventHandlers, TLGroupShape, createShapeId } from '@tldraw/tldraw' import { StateNode, TLEventHandlers, createShapeId } from '@tldraw/tldraw'
/* // There's a guide at the bottom of this file!
This is a very small example of a state node that implements a "select" tool.
The state handles two events: onPointerDown and onDoubleClick.
When the user points down on the canvas, it deselects all shapes; and when
they point a shape it selects that shape. When the user double clicks on the
canvas, it creates a new shape; and when they double click on a shape, it
deletes that shape.
*/
//[1]
export class MicroSelectTool extends StateNode { export class MicroSelectTool extends StateNode {
static override id = 'select' static override id = 'select'
//[2]
override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => { override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => {
const { editor } = this const { editor } = this
switch (info.target) { switch (info.target) {
case 'canvas': { case 'canvas': {
const hoveredShape = editor.getHoveredShape() const hitShape = editor.getShapeAtPoint(editor.inputs.currentPagePoint)
const hitShape =
hoveredShape && !this.editor.isShapeOfType<TLGroupShape>(hoveredShape, 'group')
? hoveredShape
: this.editor.getShapeAtPoint(this.editor.inputs.currentPagePoint, {
renderingOnly: true,
})
if (hitShape) { if (hitShape) {
this.onPointerDown({ this.onPointerDown({
@ -45,7 +31,7 @@ export class MicroSelectTool extends StateNode {
} }
} }
} }
//[3]
override onDoubleClick: TLEventHandlers['onDoubleClick'] = (info) => { override onDoubleClick: TLEventHandlers['onDoubleClick'] = (info) => {
const { editor } = this const { editor } = this
@ -53,6 +39,16 @@ export class MicroSelectTool extends StateNode {
switch (info.target) { switch (info.target) {
case 'canvas': { case 'canvas': {
const hitShape = editor.getShapeAtPoint(editor.inputs.currentPagePoint)
if (hitShape) {
this.onDoubleClick({
...info,
shape: hitShape,
target: 'shape',
})
return
}
const { currentPagePoint } = editor.inputs const { currentPagePoint } = editor.inputs
editor.createShapes([ editor.createShapes([
{ {
@ -75,3 +71,33 @@ export class MicroSelectTool extends StateNode {
} }
} }
} }
/*
This is a very small example of a state node that implements a "select" tool. It
doesn't implement any children states.
The state handles two events: onPointerDown [2] and onDoubleClick [2].
When the user points down on the canvas, it deselects all shapes; and when
they point a shape it selects that shape. When the user double clicks on the
canvas, it creates a new shape; and when they double click on a shape, it
deletes that shape.
[1]
This is where we define our state node by extending the StateNode class. Since
there are no children states We can simply give it an id and define methods we
want to override to handle events.
[2] onPointerDown
The user clicked on something, let's figure out what it was. We can
access the editor via this.editor, and then use it to check if we hit
a shape. If we did then we call the onPointerDown method again with the
shape as the target, select the shape, and return. If we didn't hit a
shape then we deselect all shapes.
[3] onDoubleClick
The user double clicked on something, let's do the same as above. If we
hit a shape then we call the onDoubleClick method again with the shape as
the target, delete it, and return. If we didn't hit a shape then we create
a new shape at the pointer's position.
*/

View file

@ -1,14 +1,19 @@
import { BaseBoxShapeUtil, HTMLContainer, TLBaseShape } from '@tldraw/tldraw' import { BaseBoxShapeUtil, HTMLContainer, TLBaseShape } from '@tldraw/tldraw'
// There's a guide at the bottom of this page!
// [1]
export type MiniBoxShape = TLBaseShape<'box', { w: number; h: number; color: string }> export type MiniBoxShape = TLBaseShape<'box', { w: number; h: number; color: string }>
// [2]
export class MiniBoxShapeUtil extends BaseBoxShapeUtil<MiniBoxShape> { export class MiniBoxShapeUtil extends BaseBoxShapeUtil<MiniBoxShape> {
//[a]
static override type = 'box' static override type = 'box'
//[b]
override getDefaultProps(): MiniBoxShape['props'] { override getDefaultProps(): MiniBoxShape['props'] {
return { w: 100, h: 100, color: '#efefef' } return { w: 100, h: 100, color: '#efefef' }
} }
//[c]
component(shape: MiniBoxShape) { component(shape: MiniBoxShape) {
return ( return (
<HTMLContainer> <HTMLContainer>
@ -24,8 +29,27 @@ export class MiniBoxShapeUtil extends BaseBoxShapeUtil<MiniBoxShape> {
</HTMLContainer> </HTMLContainer>
) )
} }
//[d]
indicator(shape: MiniBoxShape) { indicator(shape: MiniBoxShape) {
return <rect width={shape.props.w} height={shape.props.h} /> return <rect width={shape.props.w} height={shape.props.h} />
} }
} }
/*
This is our shape util, in tldraw all shapes extend the shape util class. In this
example we're extending the built-in BaseBoxShapeUtil class. This class provides
the functionality for our shape.
[1]
The type for our shape, we can extend the built-in TLBaseShape generic to create ours.
[2]
The shape util itself.
[a] The type of shape this util is for, this should be the same as the first argument
to the TLBaseShape generic.
[b] The default props for our shape. These will be used when creating a new shape.
[c] The component for our shape. This returns JSX and is what will be rendered on the
canvas. The HtmlContainer component is a div that provides some useful styles.
[d] The indicator for our shape, this also returns JSX. This is what will be rendered
on the canvas when the shape is selected.
*/

View file

@ -1,39 +1,23 @@
import { import { StateNode, TLEventHandlers, TLUnknownShape, createShapeId } from '@tldraw/tldraw'
StateNode, // There's a guide at the bottom of this file!
TLEventHandlers,
TLGroupShape,
TLUnknownShape,
createShapeId,
} from '@tldraw/tldraw'
/*
This is a bigger example of a state node that implements a "select" tool.
The state has three children: idle, pointing, and dragging. Only one child
state can be "active" at a time. The parent state's initial active state is
"idle". Certain events received by the child states will cause the parent
state to transition to another child state, making that state active instead.
Note that when `transition()` is called, the parent state will call the new
active state(s)'s `onEnter` method with the second argument passed to the
transition method. This is useful for passing data between states.
*/
//[1]
export class MiniSelectTool extends StateNode {
static override id = 'select'
static override children = () => [IdleState, PointingState, DraggingState]
static override initial = 'idle'
}
//[2]
class IdleState extends StateNode { class IdleState extends StateNode {
static override id = 'idle' static override id = 'idle'
//[a]
override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => { override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => {
const { editor } = this const { editor } = this
switch (info.target) { switch (info.target) {
case 'canvas': { case 'canvas': {
const hoveredShape = editor.getHoveredShape() const hitShape = editor.getShapeAtPoint(editor.inputs.currentPagePoint)
const hitShape =
hoveredShape && !this.editor.isShapeOfType<TLGroupShape>(hoveredShape, 'group')
? hoveredShape
: this.editor.getShapeAtPoint(this.editor.inputs.currentPagePoint, {
renderingOnly: true,
})
if (hitShape) { if (hitShape) {
this.onPointerDown({ this.onPointerDown({
...info, ...info,
@ -46,10 +30,6 @@ class IdleState extends StateNode {
editor.selectNone() editor.selectNone()
break break
} }
case 'selection': {
this.parent.transition('pointing', info)
break
}
case 'shape': { case 'shape': {
if (editor.inputs.shiftKey) { if (editor.inputs.shiftKey) {
editor.select(...editor.getSelectedShapeIds(), info.shape.id) editor.select(...editor.getSelectedShapeIds(), info.shape.id)
@ -63,7 +43,7 @@ class IdleState extends StateNode {
} }
} }
} }
//[b]
override onDoubleClick: TLEventHandlers['onDoubleClick'] = (info) => { override onDoubleClick: TLEventHandlers['onDoubleClick'] = (info) => {
const { editor } = this const { editor } = this
@ -71,6 +51,16 @@ class IdleState extends StateNode {
switch (info.target) { switch (info.target) {
case 'canvas': { case 'canvas': {
const hitShape = editor.getShapeAtPoint(editor.inputs.currentPagePoint)
if (hitShape) {
this.onDoubleClick({
...info,
shape: hitShape,
target: 'shape',
})
return
}
const { currentPagePoint } = editor.inputs const { currentPagePoint } = editor.inputs
editor.createShapes([ editor.createShapes([
{ {
@ -94,13 +84,14 @@ class IdleState extends StateNode {
} }
} }
//[3]
class PointingState extends StateNode { class PointingState extends StateNode {
static override id = 'pointing' static override id = 'pointing'
//[a]
override onPointerUp: TLEventHandlers['onPointerUp'] = (info) => { override onPointerUp: TLEventHandlers['onPointerUp'] = (info) => {
this.parent.transition('idle', info) this.parent.transition('idle', info)
} }
//[b]
override onPointerMove: TLEventHandlers['onPointerUp'] = () => { override onPointerMove: TLEventHandlers['onPointerUp'] = () => {
if (this.editor.inputs.isDragging) { if (this.editor.inputs.isDragging) {
this.parent.transition('dragging', { shapes: [...this.editor.getSelectedShapes()] }) this.parent.transition('dragging', { shapes: [...this.editor.getSelectedShapes()] })
@ -108,19 +99,20 @@ class PointingState extends StateNode {
} }
} }
//[4]
class DraggingState extends StateNode { class DraggingState extends StateNode {
static override id = 'dragging' static override id = 'dragging'
//[a]
private initialDraggingShapes = [] as TLUnknownShape[] private initialDraggingShapes = [] as TLUnknownShape[]
//[b]
override onEnter = (info: { shapes: TLUnknownShape[] }) => { override onEnter = (info: { shapes: TLUnknownShape[] }) => {
this.initialDraggingShapes = info.shapes this.initialDraggingShapes = info.shapes
} }
//[c]
override onPointerUp: TLEventHandlers['onPointerUp'] = (info) => { override onPointerUp: TLEventHandlers['onPointerUp'] = (info) => {
this.parent.transition('idle', info) this.parent.transition('idle', info)
} }
//[d]
override onPointerMove: TLEventHandlers['onPointerUp'] = () => { override onPointerMove: TLEventHandlers['onPointerUp'] = () => {
const { initialDraggingShapes } = this const { initialDraggingShapes } = this
const { originPagePoint, currentPagePoint } = this.editor.inputs const { originPagePoint, currentPagePoint } = this.editor.inputs
@ -137,8 +129,69 @@ class DraggingState extends StateNode {
} }
} }
export class MiniSelectTool extends StateNode { /*
static override id = 'select' This is where we implement our select tool. In tldraw, tools are part of the
static override children = () => [IdleState, PointingState, DraggingState] tldraw state chart. Check out the docs for more info:
static override initial = 'idle' https://tldraw.dev/docs/editor#State-Chart
}
Our state node [1] has three children: idle [2], pointing [3], and dragging [4].
Only one child state can be "active" at a time. The parent state's initial active
state is "idle". Certain events received by the child states will cause the parent
state to transition to another child state, making that state active instead.
Note that when `transition()` is called, the parent state will call the new
active state(s)'s `onEnter` method with the second argument passed to the
transition method. This is useful for passing data between states.
[1]
This is where we define our state node by extending the StateNode class. We
give it an id, a list of children states, and its initial active state.
[2]
The idle state is the tool's default state. This is where most of the action is.
We have some handy methods available to help us handle events:
[a] onPointerDown
The user clicked on something, let's figure out what it was. We can
access the editor via this.editor, and then use it to check if we hit
a shape. If we did then we call the onPointerDown method again with the
shape as the target, select the shape and transition to the pointing state.
Otherwise we deselect everything.
[b] onDoubleClick
The user double clicked on something, let's do the same thing as above.
If we hit a shape then we call the onDoubleClick method again with the
shape as the target, and delete the shape. Otherwise we create a new shape.
[3]
The pointing state is something of a transitionary state. Its job is to transition
to the dragging state when the user starts dragging, or go back to the idle state
on pointer up.
[a] onPointerUp
The user let go of the mouse, let's go back to the idle state.
[b] onPointerMove
The user moved the mouse, let's double check they're dragging. If they are
then let's transition to the dragging state and pass it the shapes that
are being dragged.
[4]
The dragging state is where we actually move the shapes around. It's job is to
update the position of the shapes being dragged, and transition back to the idle
state when the user lets go of the mouse.
[a] initialDraggingShapes
We'll use this to keep track of the shapes being dragged when we enter
the state.
[b] onEnter
When we enter the dragging state, we'll save the shapes being dragged.
[c] onPointerUp
The user let go of the mouse, let's go back to the idle state.
[d] onPointerMove
The user moved the mouse, let's update the position of the shapes being
dragged using editor.updateShapes().
*/

View file

@ -4,9 +4,13 @@ import { Editor, PositionedOnCanvas, TldrawEditor, createShapeId, track } from '
import { MiniBoxShapeUtil } from './MiniBoxShape' import { MiniBoxShapeUtil } from './MiniBoxShape'
import { MiniSelectTool } from './MiniSelectTool' import { MiniSelectTool } from './MiniSelectTool'
// There's a guide at the bottom of this page!
// [1]
const myTools = [MiniSelectTool] const myTools = [MiniSelectTool]
const myShapeUtils = [MiniBoxShapeUtil] const myShapeUtils = [MiniBoxShapeUtil]
// [2]
export default function OnlyEditorExample() { export default function OnlyEditorExample() {
return ( return (
<div className="tldraw__editor"> <div className="tldraw__editor">
@ -35,15 +39,31 @@ export default function OnlyEditorExample() {
) )
} }
/** // [3]
* This one will move with the camera, just like shapes do.
*/
const BackgroundComponent = track(() => { const BackgroundComponent = track(() => {
return ( return (
<PositionedOnCanvas x={16} y={16}> <PositionedOnCanvas x={16} y={16}>
<p>Double click to create shapes.</p> <p>Double click to create or delete shapes.</p>
<p>Click or Shift+Click to select shapes.</p> <p>Click or Shift+Click to select shapes.</p>
<p>Click and drag to move shapes.</p> <p>Click and drag to move shapes.</p>
</PositionedOnCanvas> </PositionedOnCanvas>
) )
}) })
/*
This example shows how to use the TldrawEditor component on its own. This is useful if you want to
create your own custom UI, shape and tool interactions.
[1]
We create a custom tool and shape util arrays. These are arrays of classes that extend
the built-in state node and shape util classes. Check out MiniSelectTool.ts and
MiniBoxShapeUtil.tsx to see how they work. Or check out the custom config example for
a more in-depth look at how to create custom tools and shapes.
There is an even simpler implementation of the select tool in MicroSelectTool.tsx, but it
isn't used in this example.
[2]
We pass our custom tools and shape utils to the TldrawEditor component. We also pass in our custom
background component to the background prop and set the initial state to the 'select' tool.
*/

View file

@ -218,7 +218,7 @@ This is where we define the shape's props and a type validator for each key. tld
validators for us to use. We can also define our own, at the moment our handle validator just returns true validators for us to use. We can also define our own, at the moment our handle validator just returns true
though, because I like to live dangerously. Props you define here will determine which style options show though, because I like to live dangerously. Props you define here will determine which style options show
up in the style menu, e.g. we define 'size' and 'color' props, but we could add 'dash', 'fill' or any other up in the style menu, e.g. we define 'size' and 'color' props, but we could add 'dash', 'fill' or any other
of the defauly props. of the default props.
[3] [3]
Here is where we set the default props for our shape, this will determine how the shape looks when we Here is where we set the default props for our shape, this will determine how the shape looks when we