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:
parent
b4d343d784
commit
6c5dd85feb
5 changed files with 195 additions and 72 deletions
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
|
@ -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().
|
||||||
|
*/
|
||||||
|
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue