Add docs for tools, sessions, cleans up tools.
This commit is contained in:
parent
c9abaca8d9
commit
7d9fcc763d
11 changed files with 212 additions and 312 deletions
37
packages/tldraw/src/state/session/about-sessions.md
Normal file
37
packages/tldraw/src/state/session/about-sessions.md
Normal file
|
@ -0,0 +1,37 @@
|
|||
# Sessions
|
||||
|
||||
A session is a class that handles events for interactions that have a beginning, middle and end.
|
||||
|
||||
They contrast with Commands, such as `duplicate`, which occur once.
|
||||
|
||||
The `TLDrawState` may only have one active session at a time (`TLDrawState.session`), or it may have no session. It may never have two sessions simulataneously—if a session begins while another session is already in progress, `TLDrawState` will throw an error. In this way, sessions function similar to a set of finite states: once a session begins, it must end before a new session can begin.
|
||||
|
||||
## Creating a Session
|
||||
|
||||
Sessions are created with `TLDrawState.startSession`. In this method, sessions are creating using the `new` keyword. Every session's constructor receives the `TLDrawState` instance's current state (`TLDrawState.state`), together with any additional parameters it defines in its constructor.
|
||||
|
||||
## Life Cycle Methods
|
||||
|
||||
A session has four life-cycle methods: `start`, `update`, `cancel` and `complete`.
|
||||
|
||||
### Start
|
||||
|
||||
When a session is created using `TLDrawState.startSession`, `TLDrawState` also calls the session's `start` method, passing in the state as the only parameter. If the `start` method returns a patch, then that patch is applied to the state.
|
||||
|
||||
### Update
|
||||
|
||||
When a session is updated using `TLDrawState.updateSession`, `TLDrawState` calls the session's `update` method, again passing in the state as well as several additional parameters: `point`, `shiftKey`, `altKey`, and `metaKey`. If the `update` method returns a patch, then that patch is applied to the state.
|
||||
|
||||
A session may use whatever information is wishes internally in order to produce its update patch. Often this means saving information about the initial state, point, or initial selected shapes, in order to compare against the update's parameters. For example, `RotateSession.update` saves the center of the selection bounds, as well as the initial angle from this center to the user's initial point, in order to compare this angle against the angle from this center to the user's current point.
|
||||
|
||||
### Cancel
|
||||
|
||||
A session may be cancelled using `TLDrawState.cancelSession`. When a session is cancelled, `TLDrawState` calls the session's `cancel` method passing in the state as the only parameter. If the `cancel` method returns a patch, then that patch is applied to the state.
|
||||
|
||||
A cancel method is expected to revert any changes made to the state since the session began. For example, `RotateSession.cancel` should restore the rotations of the user's selected shapes to their original rotations. If no change has occurred (e.g. if the rotation began and was immediately cancelled) then the `cancel` method should return `undefined` so as to avoid updating the state.
|
||||
|
||||
### Complete
|
||||
|
||||
A session may be cancelled using `TLDrawState.complete`. When a session is cancelled, `TLDrawState` calls the session's `complete` method passing in the state as the only parameter. If the `complete` method returns a patch, then that patch is applied to the state; if it returns a `command`, then that command is patched and added to the state's history.
|
||||
|
||||
If the `complete` method returns a command, then it is expected that the command's `before` patch will revert any changes made to the state since the session began, including any changes introduced in the command's `after` patch.
|
|
@ -1,8 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { StateManager } from 'rko'
|
||||
import {
|
||||
TLBoundsCorner,
|
||||
TLBoundsEdge,
|
||||
TLBoundsEventHandler,
|
||||
TLBoundsHandleEventHandler,
|
||||
TLKeyboardEventHandler,
|
||||
|
@ -40,7 +38,7 @@ import {
|
|||
ExceptFirst,
|
||||
} from '~types'
|
||||
import { TLDR } from './tldr'
|
||||
import { defaultStyle } from '~shape'
|
||||
import { defaultStyle, tldrawShapeUtils } from '~shape'
|
||||
import * as Commands from './command'
|
||||
import { ArgsOfType, getSession } from './session'
|
||||
import { sample, USER_COLORS } from './utils'
|
||||
|
@ -133,16 +131,8 @@ export class TLDrawState extends StateManager<Data> {
|
|||
|
||||
session?: Session
|
||||
|
||||
pointedId?: string
|
||||
|
||||
pointedHandle?: string
|
||||
|
||||
pointedBoundsHandle?: TLBoundsCorner | TLBoundsEdge | 'rotate'
|
||||
|
||||
isCreating = false
|
||||
|
||||
selectedGroupId?: string
|
||||
|
||||
// The editor's bounding client rect
|
||||
bounds: TLBounds = {
|
||||
minX: 0,
|
||||
|
@ -172,7 +162,6 @@ export class TLDrawState extends StateManager<Data> {
|
|||
this._onMount = onMount
|
||||
|
||||
this.session = undefined
|
||||
this.pointedId = undefined
|
||||
}
|
||||
|
||||
/* -------------------- Internal -------------------- */
|
||||
|
@ -534,8 +523,12 @@ export class TLDrawState extends StateManager<Data> {
|
|||
|
||||
if (tool === this.currentTool) return this
|
||||
|
||||
this.currentTool.onExit()
|
||||
|
||||
this.currentTool = tool
|
||||
|
||||
this.currentTool.onEnter()
|
||||
|
||||
return this.patchState(
|
||||
{
|
||||
appState: {
|
||||
|
@ -571,9 +564,11 @@ export class TLDrawState extends StateManager<Data> {
|
|||
resetDocument = (): this => {
|
||||
if (this.session) return this
|
||||
this.session = undefined
|
||||
this.selectedGroupId = undefined
|
||||
this.currentTool.setStatus(TLDrawStatus.Idle)
|
||||
this.pasteInfo.offset = [0, 0]
|
||||
|
||||
this.tools = createTools(this)
|
||||
this.currentTool = this.tools.select
|
||||
|
||||
this.resetHistory()
|
||||
.clearSelectHistory()
|
||||
.loadDocument(defaultDocument)
|
||||
|
@ -770,7 +765,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
this.resetHistory()
|
||||
this.clearSelectHistory()
|
||||
this.session = undefined
|
||||
this.selectedGroupId = undefined
|
||||
|
||||
return this.replaceState(
|
||||
{
|
||||
...defaultState,
|
||||
|
@ -1617,6 +1612,10 @@ export class TLDrawState extends StateManager<Data> {
|
|||
* @param args arguments of the session's start method.
|
||||
*/
|
||||
startSession = <T extends SessionType>(type: T, ...args: ExceptFirst<ArgsOfType<T>>): this => {
|
||||
if (this.session) {
|
||||
throw Error(`Already in a session! (${this.session.constructor.name})`)
|
||||
}
|
||||
|
||||
const Session = getSession(type)
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
|
@ -1838,6 +1837,40 @@ export class TLDrawState extends StateManager<Data> {
|
|||
)
|
||||
}
|
||||
|
||||
createTextShapeAtPoint(point: number[]) {
|
||||
const {
|
||||
shapes,
|
||||
appState: { currentPageId, currentStyle },
|
||||
} = this
|
||||
|
||||
const childIndex =
|
||||
shapes.length === 0
|
||||
? 1
|
||||
: shapes
|
||||
.filter((shape) => shape.parentId === currentPageId)
|
||||
.sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1
|
||||
|
||||
const id = Utils.uniqueId()
|
||||
|
||||
const Text = tldrawShapeUtils.text
|
||||
|
||||
const newShape = Text.create({
|
||||
id,
|
||||
parentId: currentPageId,
|
||||
childIndex,
|
||||
point,
|
||||
style: { ...currentStyle },
|
||||
})
|
||||
|
||||
const bounds = Text.getBounds(newShape)
|
||||
|
||||
newShape.point = Vec.sub(newShape.point, [bounds.width / 2, bounds.height / 2])
|
||||
|
||||
this.createShapes(newShape)
|
||||
|
||||
this.setEditingId(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create one or more shapes.
|
||||
* @param shapes An array of shapes.
|
||||
|
|
|
@ -2,27 +2,11 @@ import Vec from '@tldraw/vec'
|
|||
import { Utils, TLPointerEventHandler } from '@tldraw/core'
|
||||
import { Arrow } from '~shape/shapes'
|
||||
import { SessionType, TLDrawShapeType } from '~types'
|
||||
import { BaseTool } from '../BaseTool'
|
||||
|
||||
enum Status {
|
||||
Idle = 'idle',
|
||||
Creating = 'creating',
|
||||
}
|
||||
import { BaseTool, Status } from '../BaseTool'
|
||||
|
||||
export class ArrowTool extends BaseTool {
|
||||
type = TLDrawShapeType.Arrow
|
||||
|
||||
status = Status.Idle
|
||||
|
||||
/* --------------------- Methods -------------------- */
|
||||
|
||||
onEnter = () => {
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
|
||||
onExit = () => {
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
/* ----------------- Event Handlers ----------------- */
|
||||
|
||||
onPointerDown: TLPointerEventHandler = (info) => {
|
||||
|
@ -50,19 +34,4 @@ export class ArrowTool extends BaseTool {
|
|||
|
||||
this.setStatus(Status.Creating)
|
||||
}
|
||||
|
||||
onPointerMove: TLPointerEventHandler = (info) => {
|
||||
if (this.status === Status.Creating) {
|
||||
const pagePoint = Vec.round(this.state.getPagePoint(info.point))
|
||||
this.state.updateSession(pagePoint, info.shiftKey, info.altKey, info.metaKey)
|
||||
}
|
||||
}
|
||||
|
||||
onPointerUp: TLPointerEventHandler = () => {
|
||||
if (this.status === Status.Creating) {
|
||||
this.state.completeSession()
|
||||
}
|
||||
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import Vec from '@tldraw/vec'
|
||||
import type {
|
||||
TLBoundsEventHandler,
|
||||
TLBoundsHandleEventHandler,
|
||||
|
@ -13,25 +14,41 @@ import Utils from '~../../core/src/utils'
|
|||
import type { TLDrawState } from '~state'
|
||||
import type { TLDrawShapeType } from '~types'
|
||||
|
||||
export abstract class BaseTool {
|
||||
export enum Status {
|
||||
Idle = 'idle',
|
||||
Creating = 'creating',
|
||||
Pinching = 'pinching',
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export abstract class BaseTool<T extends string = any> {
|
||||
abstract type: TLDrawShapeType | 'select'
|
||||
|
||||
state: TLDrawState
|
||||
|
||||
status: string = 'idle' as const
|
||||
|
||||
setStatus = (status: typeof this.status) => {
|
||||
this.status = status
|
||||
this.state.setStatus(this.status)
|
||||
}
|
||||
status: Status | T = Status.Idle
|
||||
|
||||
constructor(state: TLDrawState) {
|
||||
this.state = state
|
||||
}
|
||||
|
||||
abstract onEnter: () => void
|
||||
protected readonly setStatus = (status: Status | T) => {
|
||||
this.status = status as Status | T
|
||||
this.state.setStatus(this.status as string)
|
||||
}
|
||||
|
||||
abstract onExit: () => void
|
||||
onEnter = () => {
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
|
||||
onExit = () => {
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
|
||||
onCancel = () => {
|
||||
this.state.cancelSession()
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
|
||||
getNextChildIndex = () => {
|
||||
const {
|
||||
|
@ -46,19 +63,78 @@ export abstract class BaseTool {
|
|||
.sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1
|
||||
}
|
||||
|
||||
onCancel = () => {
|
||||
if (this.status === 'creating') {
|
||||
this.state.cancelSession()
|
||||
/* --------------------- Camera --------------------- */
|
||||
|
||||
onPinchStart: TLPinchEventHandler = () => {
|
||||
this.state.cancelSession()
|
||||
this.setStatus(Status.Pinching)
|
||||
}
|
||||
|
||||
onPinchEnd: TLPinchEventHandler = () => {
|
||||
if (Utils.isMobileSafari()) {
|
||||
this.state.undoSelect()
|
||||
}
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
|
||||
onPinch: TLPinchEventHandler = (info, e) => {
|
||||
if (this.status !== 'pinching') return
|
||||
this.state.pinchZoom(info.point, info.delta, info.delta[2])
|
||||
this.onPointerMove?.(info, e as unknown as React.PointerEvent)
|
||||
}
|
||||
|
||||
/* ---------------------- Keys ---------------------- */
|
||||
|
||||
onKeyDown: TLKeyboardEventHandler = (key, info) => {
|
||||
/* noop */
|
||||
if (key === 'Meta' || key === 'Control' || key === 'Alt') {
|
||||
this.state.updateSession(
|
||||
this.state.getPagePoint(info.point),
|
||||
info.shiftKey,
|
||||
info.altKey,
|
||||
info.metaKey
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
onKeyUp: TLKeyboardEventHandler = (key, info) => {
|
||||
/* noop */
|
||||
if (key === 'Meta' || key === 'Control' || key === 'Alt') {
|
||||
this.state.updateSession(
|
||||
this.state.getPagePoint(info.point),
|
||||
info.shiftKey,
|
||||
info.altKey,
|
||||
info.metaKey
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------- Pointer -------------------- */
|
||||
|
||||
onPointerMove: TLPointerEventHandler = (info) => {
|
||||
if (this.status === Status.Creating) {
|
||||
const pagePoint = Vec.round(this.state.getPagePoint(info.point))
|
||||
this.state.updateSession(pagePoint, info.shiftKey, info.altKey, info.metaKey)
|
||||
}
|
||||
}
|
||||
|
||||
onPointerUp: TLPointerEventHandler = () => {
|
||||
if (this.status === Status.Creating) {
|
||||
this.state.completeSession()
|
||||
}
|
||||
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
|
||||
/* --------------------- Others --------------------- */
|
||||
|
||||
// Camera Events
|
||||
onPan?: TLWheelEventHandler
|
||||
onZoom?: TLWheelEventHandler
|
||||
|
||||
// Pointer Events
|
||||
onPointerMove?: TLPointerEventHandler
|
||||
onPointerUp?: TLPointerEventHandler
|
||||
onPointerDown?: TLPointerEventHandler
|
||||
|
||||
// Canvas (background)
|
||||
|
@ -107,52 +183,4 @@ export abstract class BaseTool {
|
|||
// Misc
|
||||
onShapeBlur?: TLShapeBlurHandler
|
||||
onShapeClone?: TLShapeCloneHandler
|
||||
|
||||
/* --------------------- Camera --------------------- */
|
||||
|
||||
onPinchStart: TLPinchEventHandler = () => {
|
||||
this.state.cancelSession()
|
||||
this.setStatus('pinching')
|
||||
}
|
||||
|
||||
onPinchEnd: TLPinchEventHandler = () => {
|
||||
if (Utils.isMobileSafari()) {
|
||||
this.state.undoSelect()
|
||||
}
|
||||
this.setStatus('idle')
|
||||
}
|
||||
|
||||
onPinch: TLPinchEventHandler = (info, e) => {
|
||||
if (this.status !== 'pinching') return
|
||||
this.state.pinchZoom(info.point, info.delta, info.delta[2])
|
||||
this.onPointerMove?.(info, e as unknown as React.PointerEvent)
|
||||
}
|
||||
|
||||
/* ---------------------- Keys ---------------------- */
|
||||
|
||||
onKeyDown: TLKeyboardEventHandler = (key, info) => {
|
||||
/* noop */
|
||||
if (key === 'Meta' || key === 'Control' || key === 'Alt') {
|
||||
this.state.updateSession(
|
||||
this.state.getPagePoint(info.point),
|
||||
info.shiftKey,
|
||||
info.altKey,
|
||||
info.metaKey
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
onKeyUp: TLKeyboardEventHandler = (key, info) => {
|
||||
/* noop */
|
||||
if (key === 'Meta' || key === 'Control' || key === 'Alt') {
|
||||
this.state.updateSession(
|
||||
this.state.getPagePoint(info.point),
|
||||
info.shiftKey,
|
||||
info.altKey,
|
||||
info.metaKey
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,28 +3,11 @@ import type { TLPointerEventHandler } from '~../../core/src/types'
|
|||
import Utils from '~../../core/src/utils'
|
||||
import { Draw } from '~shape/shapes'
|
||||
import { SessionType, TLDrawShapeType } from '~types'
|
||||
import { BaseTool } from '../BaseTool'
|
||||
|
||||
enum Status {
|
||||
Idle = 'idle',
|
||||
Creating = 'creating',
|
||||
}
|
||||
import { BaseTool, Status } from '../BaseTool'
|
||||
|
||||
export class DrawTool extends BaseTool {
|
||||
type = TLDrawShapeType.Draw
|
||||
|
||||
status = Status.Idle
|
||||
|
||||
/* --------------------- Methods -------------------- */
|
||||
|
||||
onEnter = () => {
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
|
||||
onExit = () => {
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
|
||||
/* ----------------- Event Handlers ----------------- */
|
||||
|
||||
onPointerDown: TLPointerEventHandler = (info) => {
|
||||
|
|
|
@ -2,27 +2,11 @@ import Vec from '@tldraw/vec'
|
|||
import { Utils, TLPointerEventHandler, TLKeyboardEventHandler, TLBoundsCorner } from '@tldraw/core'
|
||||
import { Ellipse } from '~shape/shapes'
|
||||
import { SessionType, TLDrawShapeType } from '~types'
|
||||
import { BaseTool } from '../BaseTool'
|
||||
import { BaseTool, Status } from '../BaseTool'
|
||||
|
||||
enum Status {
|
||||
Idle = 'idle',
|
||||
Creating = 'creating',
|
||||
}
|
||||
export class EllipseTool extends BaseTool {
|
||||
type = TLDrawShapeType.Ellipse
|
||||
|
||||
status = Status.Idle
|
||||
|
||||
/* --------------------- Methods -------------------- */
|
||||
|
||||
onEnter = () => {
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
|
||||
onExit = () => {
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
|
||||
/* ----------------- Event Handlers ----------------- */
|
||||
|
||||
onPointerDown: TLPointerEventHandler = (info) => {
|
||||
|
@ -55,31 +39,4 @@ export class EllipseTool extends BaseTool {
|
|||
|
||||
this.setStatus(Status.Creating)
|
||||
}
|
||||
|
||||
onPointerMove: TLPointerEventHandler = (info) => {
|
||||
if (this.status === Status.Creating) {
|
||||
const pagePoint = Vec.round(this.state.getPagePoint(info.point))
|
||||
this.state.updateSession(pagePoint, info.shiftKey, info.altKey, info.metaKey)
|
||||
}
|
||||
}
|
||||
|
||||
onKeyDown: TLKeyboardEventHandler = (key, info) => {
|
||||
if (
|
||||
(this.status === Status.Creating && key === 'Shift') ||
|
||||
key === 'Meta' ||
|
||||
key === 'Alt' ||
|
||||
key === 'Ctrl'
|
||||
) {
|
||||
const pagePoint = Vec.round(this.state.getPagePoint(info.point))
|
||||
this.state.updateSession(pagePoint, info.shiftKey, info.altKey, info.metaKey)
|
||||
}
|
||||
}
|
||||
|
||||
onPointerUp: TLPointerEventHandler = () => {
|
||||
if (this.status === Status.Creating) {
|
||||
this.state.completeSession()
|
||||
}
|
||||
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,12 @@
|
|||
import Vec from '@tldraw/vec'
|
||||
import { Utils, TLPointerEventHandler, TLKeyboardEventHandler, TLBoundsCorner } from '@tldraw/core'
|
||||
import { Utils, TLPointerEventHandler, TLBoundsCorner } from '@tldraw/core'
|
||||
import { Rectangle } from '~shape/shapes'
|
||||
import { SessionType, TLDrawShapeType } from '~types'
|
||||
import { BaseTool } from '../BaseTool'
|
||||
|
||||
enum Status {
|
||||
Idle = 'idle',
|
||||
Creating = 'creating',
|
||||
}
|
||||
import { BaseTool, Status } from '../BaseTool'
|
||||
|
||||
export class RectangleTool extends BaseTool {
|
||||
type = TLDrawShapeType.Rectangle
|
||||
|
||||
status = Status.Idle
|
||||
|
||||
/* --------------------- Methods -------------------- */
|
||||
|
||||
onEnter = () => {
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
|
||||
onExit = () => {
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
|
||||
/* ----------------- Event Handlers ----------------- */
|
||||
|
||||
onPointerDown: TLPointerEventHandler = (info) => {
|
||||
|
@ -56,31 +39,4 @@ export class RectangleTool extends BaseTool {
|
|||
|
||||
this.setStatus(Status.Creating)
|
||||
}
|
||||
|
||||
onPointerMove: TLPointerEventHandler = (info) => {
|
||||
if (this.status === Status.Creating) {
|
||||
const pagePoint = Vec.round(this.state.getPagePoint(info.point))
|
||||
this.state.updateSession(pagePoint, info.shiftKey, info.altKey, info.metaKey)
|
||||
}
|
||||
}
|
||||
|
||||
onKeyDown: TLKeyboardEventHandler = (key, info) => {
|
||||
if (
|
||||
(this.status === Status.Creating && key === 'Shift') ||
|
||||
key === 'Meta' ||
|
||||
key === 'Alt' ||
|
||||
key === 'Ctrl'
|
||||
) {
|
||||
const pagePoint = Vec.round(this.state.getPagePoint(info.point))
|
||||
this.state.updateSession(pagePoint, info.shiftKey, info.altKey, info.metaKey)
|
||||
}
|
||||
}
|
||||
|
||||
onPointerUp: TLPointerEventHandler = () => {
|
||||
if (this.status === Status.Creating) {
|
||||
this.state.completeSession()
|
||||
}
|
||||
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ import { TLDR } from '~state/tldr'
|
|||
|
||||
enum Status {
|
||||
Idle = 'idle',
|
||||
Creating = 'creating',
|
||||
Pinching = 'pinching',
|
||||
PointingCanvas = 'pointingCanvas',
|
||||
PointingHandle = 'pointingHandle',
|
||||
PointingBounds = 'pointingBounds',
|
||||
|
@ -26,18 +28,15 @@ enum Status {
|
|||
Translating = 'translating',
|
||||
Transforming = 'transforming',
|
||||
Rotating = 'rotating',
|
||||
Pinching = 'pinching',
|
||||
Brushing = 'brushing',
|
||||
GridCloning = 'gridCloning',
|
||||
ClonePainting = 'clonePainting',
|
||||
SpacePanning = 'spacePanning',
|
||||
}
|
||||
|
||||
export class SelectTool extends BaseTool {
|
||||
export class SelectTool extends BaseTool<Status> {
|
||||
type = 'select' as const
|
||||
|
||||
status: Status = Status.Idle
|
||||
|
||||
pointedId?: string
|
||||
|
||||
selectedGroupId?: string
|
||||
|
@ -162,7 +161,6 @@ export class SelectTool extends BaseTool {
|
|||
|
||||
onCancel = () => {
|
||||
this.deselectAll()
|
||||
// TODO: Make all cancel sessions have no arguments
|
||||
this.state.cancelSession()
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
|
@ -422,9 +420,8 @@ export class SelectTool extends BaseTool {
|
|||
onDoubleClickCanvas: TLCanvasEventHandler = (info) => {
|
||||
const pagePoint = this.state.getPagePoint(info.point)
|
||||
this.state.selectTool(TLDrawShapeType.Text)
|
||||
const tool = this.state.tools[TLDrawShapeType.Text]
|
||||
this.setStatus(Status.Idle)
|
||||
tool.createTextShapeAtPoint(pagePoint)
|
||||
this.state.createTextShapeAtPoint(pagePoint)
|
||||
}
|
||||
|
||||
// Shape
|
||||
|
|
|
@ -3,30 +3,13 @@ import type { TLPointerEventHandler } from '@tldraw/core'
|
|||
import { Utils } from '@tldraw/core'
|
||||
import { Sticky } from '~shape/shapes'
|
||||
import { SessionType, TLDrawShapeType } from '~types'
|
||||
import { BaseTool } from '../BaseTool'
|
||||
|
||||
enum Status {
|
||||
Idle = 'idle',
|
||||
Creating = 'creating',
|
||||
}
|
||||
import { BaseTool, Status } from '../BaseTool'
|
||||
|
||||
export class StickyTool extends BaseTool {
|
||||
type = TLDrawShapeType.Sticky
|
||||
|
||||
status = Status.Idle
|
||||
|
||||
shapeId?: string
|
||||
|
||||
/* --------------------- Methods -------------------- */
|
||||
|
||||
onEnter = () => {
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
|
||||
onExit = () => {
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
|
||||
/* ----------------- Event Handlers ----------------- */
|
||||
|
||||
onPointerDown: TLPointerEventHandler = (info) => {
|
||||
|
@ -44,16 +27,10 @@ export class StickyTool extends BaseTool {
|
|||
const pagePoint = Vec.round(this.state.getPagePoint(info.point))
|
||||
|
||||
const {
|
||||
shapes,
|
||||
appState: { currentPageId, currentStyle },
|
||||
} = this.state
|
||||
|
||||
const childIndex =
|
||||
shapes.length === 0
|
||||
? 1
|
||||
: shapes
|
||||
.filter((shape) => shape.parentId === currentPageId)
|
||||
.sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1
|
||||
const childIndex = this.getNextChildIndex()
|
||||
|
||||
const id = Utils.uniqueId()
|
||||
|
||||
|
@ -79,13 +56,6 @@ export class StickyTool extends BaseTool {
|
|||
}
|
||||
}
|
||||
|
||||
onPointerMove: TLPointerEventHandler = (info) => {
|
||||
if (this.status === Status.Creating) {
|
||||
const pagePoint = Vec.round(this.state.getPagePoint(info.point))
|
||||
this.state.updateSession(pagePoint, info.shiftKey, info.altKey, info.metaKey)
|
||||
}
|
||||
}
|
||||
|
||||
onPointerUp: TLPointerEventHandler = () => {
|
||||
if (this.status === Status.Creating) {
|
||||
this.state.completeSession()
|
||||
|
|
|
@ -1,29 +1,13 @@
|
|||
import Vec from '@tldraw/vec'
|
||||
import { Utils, TLPointerEventHandler, TLKeyboardEventHandler } from '@tldraw/core'
|
||||
import { Text } from '~shape/shapes'
|
||||
import type { TLPointerEventHandler, TLKeyboardEventHandler } from '@tldraw/core'
|
||||
import { TLDrawShapeType } from '~types'
|
||||
import { BaseTool } from '../BaseTool'
|
||||
|
||||
enum Status {
|
||||
Idle = 'idle',
|
||||
Creating = 'creating',
|
||||
}
|
||||
import { BaseTool, Status } from '../BaseTool'
|
||||
|
||||
export class TextTool extends BaseTool {
|
||||
type = TLDrawShapeType.Text
|
||||
|
||||
status = Status.Idle
|
||||
|
||||
/* --------------------- Methods -------------------- */
|
||||
|
||||
onEnter = () => {
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
|
||||
onExit = () => {
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
|
||||
stopEditingShape = () => {
|
||||
this.setStatus(Status.Idle)
|
||||
|
||||
|
@ -32,54 +16,21 @@ export class TextTool extends BaseTool {
|
|||
}
|
||||
}
|
||||
|
||||
createTextShapeAtPoint = (point: number[]) => {
|
||||
const {
|
||||
shapes,
|
||||
appState: { currentPageId, currentStyle },
|
||||
} = this.state
|
||||
|
||||
const childIndex =
|
||||
shapes.length === 0
|
||||
? 1
|
||||
: shapes
|
||||
.filter((shape) => shape.parentId === currentPageId)
|
||||
.sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1
|
||||
|
||||
const id = Utils.uniqueId()
|
||||
|
||||
const newShape = Text.create({
|
||||
id,
|
||||
parentId: currentPageId,
|
||||
childIndex,
|
||||
point,
|
||||
style: { ...currentStyle },
|
||||
})
|
||||
|
||||
const bounds = Text.getBounds(newShape)
|
||||
|
||||
newShape.point = Vec.sub(newShape.point, [bounds.width / 2, bounds.height / 2])
|
||||
|
||||
this.state.createShapes(newShape)
|
||||
|
||||
this.state.setEditingId(id)
|
||||
|
||||
this.setStatus(Status.Creating)
|
||||
}
|
||||
|
||||
/* ----------------- Event Handlers ----------------- */
|
||||
|
||||
onKeyUp: TLKeyboardEventHandler = (key, info) => {
|
||||
onKeyUp: TLKeyboardEventHandler = () => {
|
||||
// noop
|
||||
}
|
||||
|
||||
onKeyDown: TLKeyboardEventHandler = (key, info) => {
|
||||
onKeyDown: TLKeyboardEventHandler = () => {
|
||||
// noop
|
||||
}
|
||||
|
||||
onPointerDown: TLPointerEventHandler = (info) => {
|
||||
if (this.status === Status.Idle) {
|
||||
const pagePoint = Vec.round(this.state.getPagePoint(info.point))
|
||||
this.createTextShapeAtPoint(pagePoint)
|
||||
const point = Vec.round(this.state.getPagePoint(info.point))
|
||||
this.state.createTextShapeAtPoint(point)
|
||||
this.setStatus(Status.Creating)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
19
packages/tldraw/src/state/tool/about-tools.md
Normal file
19
packages/tldraw/src/state/tool/about-tools.md
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Tools
|
||||
|
||||
Tools are classes that handle events. A TLDrawState instance has a set of tools (`tools`) and one current tool (`currentTool`). The state delegates events (such as `onPointerMove`) to its current tool for handling.
|
||||
|
||||
In this way, tools function as a finite state machine: events are always handled by a tool and will only ever be handled by one tool.
|
||||
|
||||
## BaseTool
|
||||
|
||||
Each tool extends `BaseTool`, which comes with several default methods used by the majority of other tools. If a tool overrides one of the BaseTool methods, consider re-implementing the functionality found in BaseTool. For example, see how `StickyTool` overrides `onPointerUp` so that, in addition to completing the current session, the it also sets the state's `editingId` to the new sticky shape.
|
||||
|
||||
## Enter and Exit Methods
|
||||
|
||||
When the state changes from one tool to another, it will:
|
||||
|
||||
1. run the previous tool's `onExit` method
|
||||
2. switch to the new tool
|
||||
3. run the new current tool's `onEnter` method
|
||||
|
||||
Each tool has a status (`status`) that may be set with `setStatus`.
|
Loading…
Reference in a new issue