diff --git a/apps/examples/src/examples/only-editor/MicroSelectTool.ts b/apps/examples/src/examples/only-editor/MicroSelectTool.ts index 2f3836366..fc0636f86 100644 --- a/apps/examples/src/examples/only-editor/MicroSelectTool.ts +++ b/apps/examples/src/examples/only-editor/MicroSelectTool.ts @@ -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(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. +*/ diff --git a/apps/examples/src/examples/only-editor/MiniBoxShape.tsx b/apps/examples/src/examples/only-editor/MiniBoxShape.tsx index 902d29d06..7dc8c2055 100644 --- a/apps/examples/src/examples/only-editor/MiniBoxShape.tsx +++ b/apps/examples/src/examples/only-editor/MiniBoxShape.tsx @@ -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 { + //[a] static override type = 'box' - + //[b] override getDefaultProps(): MiniBoxShape['props'] { return { w: 100, h: 100, color: '#efefef' } } - + //[c] component(shape: MiniBoxShape) { return ( @@ -24,8 +29,27 @@ export class MiniBoxShapeUtil extends BaseBoxShapeUtil { ) } - + //[d] indicator(shape: MiniBoxShape) { return } } + +/* +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. +*/ diff --git a/apps/examples/src/examples/only-editor/MiniSelectTool.ts b/apps/examples/src/examples/only-editor/MiniSelectTool.ts index c7005e0b4..45f699058 100644 --- a/apps/examples/src/examples/only-editor/MiniSelectTool.ts +++ b/apps/examples/src/examples/only-editor/MiniSelectTool.ts @@ -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(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(). +*/ diff --git a/apps/examples/src/examples/only-editor/OnlyEditor.tsx b/apps/examples/src/examples/only-editor/OnlyEditor.tsx index 0063e0d63..b6fd1fc2f 100644 --- a/apps/examples/src/examples/only-editor/OnlyEditor.tsx +++ b/apps/examples/src/examples/only-editor/OnlyEditor.tsx @@ -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 (
@@ -35,15 +39,31 @@ export default function OnlyEditorExample() { ) } -/** - * This one will move with the camera, just like shapes do. - */ +// [3] const BackgroundComponent = track(() => { return ( -

Double click to create shapes.

+

Double click to create or delete shapes.

Click or Shift+Click to select shapes.

Click and drag to move shapes.

) }) + +/* +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. +*/ diff --git a/apps/examples/src/examples/speech-bubble/SpeechBubble/SpeechBubbleUtil.tsx b/apps/examples/src/examples/speech-bubble/SpeechBubble/SpeechBubbleUtil.tsx index de55a1392..760b8473d 100644 --- a/apps/examples/src/examples/speech-bubble/SpeechBubble/SpeechBubbleUtil.tsx +++ b/apps/examples/src/examples/speech-bubble/SpeechBubble/SpeechBubbleUtil.tsx @@ -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