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'
|
||||
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
// There's a guide at the bottom of this file!
|
||||
|
||||
//[1]
|
||||
export class MicroSelectTool extends StateNode {
|
||||
static override id = 'select'
|
||||
|
||||
//[2]
|
||||
override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => {
|
||||
const { editor } = this
|
||||
|
||||
switch (info.target) {
|
||||
case 'canvas': {
|
||||
const hoveredShape = editor.getHoveredShape()
|
||||
const hitShape =
|
||||
hoveredShape && !this.editor.isShapeOfType<TLGroupShape>(hoveredShape, 'group')
|
||||
? hoveredShape
|
||||
: this.editor.getShapeAtPoint(this.editor.inputs.currentPagePoint, {
|
||||
renderingOnly: true,
|
||||
})
|
||||
const hitShape = editor.getShapeAtPoint(editor.inputs.currentPagePoint)
|
||||
|
||||
if (hitShape) {
|
||||
this.onPointerDown({
|
||||
|
@ -45,7 +31,7 @@ export class MicroSelectTool extends StateNode {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
//[3]
|
||||
override onDoubleClick: TLEventHandlers['onDoubleClick'] = (info) => {
|
||||
const { editor } = this
|
||||
|
||||
|
@ -53,6 +39,16 @@ export class MicroSelectTool extends StateNode {
|
|||
|
||||
switch (info.target) {
|
||||
case 'canvas': {
|
||||
const hitShape = editor.getShapeAtPoint(editor.inputs.currentPagePoint)
|
||||
|
||||
if (hitShape) {
|
||||
this.onDoubleClick({
|
||||
...info,
|
||||
shape: hitShape,
|
||||
target: 'shape',
|
||||
})
|
||||
return
|
||||
}
|
||||
const { currentPagePoint } = editor.inputs
|
||||
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'
|
||||
|
||||
// There's a guide at the bottom of this page!
|
||||
|
||||
// [1]
|
||||
export type MiniBoxShape = TLBaseShape<'box', { w: number; h: number; color: string }>
|
||||
|
||||
// [2]
|
||||
export class MiniBoxShapeUtil extends BaseBoxShapeUtil<MiniBoxShape> {
|
||||
//[a]
|
||||
static override type = 'box'
|
||||
|
||||
//[b]
|
||||
override getDefaultProps(): MiniBoxShape['props'] {
|
||||
return { w: 100, h: 100, color: '#efefef' }
|
||||
}
|
||||
|
||||
//[c]
|
||||
component(shape: MiniBoxShape) {
|
||||
return (
|
||||
<HTMLContainer>
|
||||
|
@ -24,8 +29,27 @@ export class MiniBoxShapeUtil extends BaseBoxShapeUtil<MiniBoxShape> {
|
|||
</HTMLContainer>
|
||||
)
|
||||
}
|
||||
|
||||
//[d]
|
||||
indicator(shape: MiniBoxShape) {
|
||||
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 {
|
||||
StateNode,
|
||||
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.
|
||||
*/
|
||||
import { StateNode, TLEventHandlers, TLUnknownShape, createShapeId } from '@tldraw/tldraw'
|
||||
// There's a guide at the bottom of this file!
|
||||
|
||||
//[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 {
|
||||
static override id = 'idle'
|
||||
|
||||
//[a]
|
||||
override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => {
|
||||
const { editor } = this
|
||||
|
||||
switch (info.target) {
|
||||
case 'canvas': {
|
||||
const hoveredShape = editor.getHoveredShape()
|
||||
const hitShape =
|
||||
hoveredShape && !this.editor.isShapeOfType<TLGroupShape>(hoveredShape, 'group')
|
||||
? hoveredShape
|
||||
: this.editor.getShapeAtPoint(this.editor.inputs.currentPagePoint, {
|
||||
renderingOnly: true,
|
||||
})
|
||||
const hitShape = editor.getShapeAtPoint(editor.inputs.currentPagePoint)
|
||||
|
||||
if (hitShape) {
|
||||
this.onPointerDown({
|
||||
...info,
|
||||
|
@ -46,10 +30,6 @@ class IdleState extends StateNode {
|
|||
editor.selectNone()
|
||||
break
|
||||
}
|
||||
case 'selection': {
|
||||
this.parent.transition('pointing', info)
|
||||
break
|
||||
}
|
||||
case 'shape': {
|
||||
if (editor.inputs.shiftKey) {
|
||||
editor.select(...editor.getSelectedShapeIds(), info.shape.id)
|
||||
|
@ -63,7 +43,7 @@ class IdleState extends StateNode {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
//[b]
|
||||
override onDoubleClick: TLEventHandlers['onDoubleClick'] = (info) => {
|
||||
const { editor } = this
|
||||
|
||||
|
@ -71,6 +51,16 @@ class IdleState extends StateNode {
|
|||
|
||||
switch (info.target) {
|
||||
case 'canvas': {
|
||||
const hitShape = editor.getShapeAtPoint(editor.inputs.currentPagePoint)
|
||||
|
||||
if (hitShape) {
|
||||
this.onDoubleClick({
|
||||
...info,
|
||||
shape: hitShape,
|
||||
target: 'shape',
|
||||
})
|
||||
return
|
||||
}
|
||||
const { currentPagePoint } = editor.inputs
|
||||
editor.createShapes([
|
||||
{
|
||||
|
@ -94,13 +84,14 @@ class IdleState extends StateNode {
|
|||
}
|
||||
}
|
||||
|
||||
//[3]
|
||||
class PointingState extends StateNode {
|
||||
static override id = 'pointing'
|
||||
|
||||
//[a]
|
||||
override onPointerUp: TLEventHandlers['onPointerUp'] = (info) => {
|
||||
this.parent.transition('idle', info)
|
||||
}
|
||||
|
||||
//[b]
|
||||
override onPointerMove: TLEventHandlers['onPointerUp'] = () => {
|
||||
if (this.editor.inputs.isDragging) {
|
||||
this.parent.transition('dragging', { shapes: [...this.editor.getSelectedShapes()] })
|
||||
|
@ -108,19 +99,20 @@ class PointingState extends StateNode {
|
|||
}
|
||||
}
|
||||
|
||||
//[4]
|
||||
class DraggingState extends StateNode {
|
||||
static override id = 'dragging'
|
||||
|
||||
//[a]
|
||||
private initialDraggingShapes = [] as TLUnknownShape[]
|
||||
|
||||
//[b]
|
||||
override onEnter = (info: { shapes: TLUnknownShape[] }) => {
|
||||
this.initialDraggingShapes = info.shapes
|
||||
}
|
||||
|
||||
//[c]
|
||||
override onPointerUp: TLEventHandlers['onPointerUp'] = (info) => {
|
||||
this.parent.transition('idle', info)
|
||||
}
|
||||
|
||||
//[d]
|
||||
override onPointerMove: TLEventHandlers['onPointerUp'] = () => {
|
||||
const { initialDraggingShapes } = this
|
||||
const { originPagePoint, currentPagePoint } = this.editor.inputs
|
||||
|
@ -137,8 +129,69 @@ class DraggingState extends StateNode {
|
|||
}
|
||||
}
|
||||
|
||||
export class MiniSelectTool extends StateNode {
|
||||
static override id = 'select'
|
||||
static override children = () => [IdleState, PointingState, DraggingState]
|
||||
static override initial = 'idle'
|
||||
}
|
||||
/*
|
||||
This is where we implement our select tool. In tldraw, tools are part of the
|
||||
tldraw state chart. Check out the docs for more info:
|
||||
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 { MiniSelectTool } from './MiniSelectTool'
|
||||
|
||||
// There's a guide at the bottom of this page!
|
||||
|
||||
// [1]
|
||||
const myTools = [MiniSelectTool]
|
||||
const myShapeUtils = [MiniBoxShapeUtil]
|
||||
|
||||
// [2]
|
||||
export default function OnlyEditorExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
|
@ -35,15 +39,31 @@ export default function OnlyEditorExample() {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This one will move with the camera, just like shapes do.
|
||||
*/
|
||||
// [3]
|
||||
const BackgroundComponent = track(() => {
|
||||
return (
|
||||
<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 and drag to move shapes.</p>
|
||||
</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
|
||||
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
|
||||
of the defauly props.
|
||||
of the default props.
|
||||
|
||||
[3]
|
||||
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