My App
BoardKitCore

Tools

Abstract Tool class, tool state machine, and ToolRegistry

Tools process pointer input events and produce element mutations and preview elements. Each tool follows a state machine pattern: idle → active → finishing → idle.

Tool Abstract Class

abstract class Tool {
    abstract readonly id: string;
    abstract readonly name: string;
    abstract state: ToolState;

    abstract onPointerDown(event: InputEvent, scene: SceneState): ToolResult;
    abstract onPointerMove(event: InputEvent, scene: SceneState): ToolResult;
    abstract onPointerUp(event: InputEvent, scene: SceneState): ToolResult;
    abstract onCancel(): ToolResult;
}

ToolState

type ToolState = 'idle' | 'active' | 'finishing';
StateDescription
idleTool is not processing input
activeTool is actively processing (e.g., user is drawing)
finishingTool is completing its operation (e.g., finalizing a shape)

InputEvent

The normalized pointer event that tools receive:

interface InputEvent {
    type: 'pointerDown' | 'pointerMove' | 'pointerUp' | 'pointerCancel';
    position: Point;
    pressure?: number;
    tilt?: { x: number; y: number };
    button: number;
    modifiers: {
        shift: boolean;
        ctrl: boolean;
        alt: boolean;
        meta: boolean;
    };
    timestamp: number;
}

ToolResult

The output of a tool's event handler:

interface ToolResult {
    mutations?: ElementMutation[];
    preview?: Element[];
    cursor?: string;
    state: ToolState;
}
FieldDescription
mutationsElement mutations to apply to the scene (create, update, delete)
previewTemporary elements to render on the interactive layer (e.g., in-progress stroke)
cursorCSS cursor to display
stateThe tool's new state after processing

ToolRegistry

Manages registered tools and provides lookup.

class ToolRegistry {
    register(tool: Tool): void;
    get(id: string): Tool | undefined;
    getAll(): Tool[];
    static createDefault(): ToolRegistry;
}

createDefault() registers all 8 built-in tools:

import { ToolRegistry, TOOL_IDS } from '@hfu.digital/boardkit-core';

const registry = ToolRegistry.createDefault();
const pen = registry.get(TOOL_IDS.PEN);       // PenTool
const shape = registry.get(TOOL_IDS.SHAPE);   // ShapeTool
const text = registry.get(TOOL_IDS.TEXT);      // TextTool

Built-in Tools

ToolIDCreates ElementDescription
PenToolpenStrokeElementFreehand drawing with pressure support
ShapeToolshapeShapeElementRectangle, ellipse, line, arrow, triangle
TextTooltextTextElementClick to create a text box
StickyNoteToolstickyNoteStickyNoteElementClick to create a colored sticky note
ConnectorToolconnectorConnectorElementClick two elements to connect them
SelectToolselect--Click/drag to select and move elements
EraserTooleraser--Click/drag to delete elements
LaserToollaser--Preview-only laser pointer (no mutations)

TOOL_IDS Constants

const TOOL_IDS = {
    PEN: 'pen',
    SHAPE: 'shape',
    SELECT: 'select',
    ERASER: 'eraser',
    TEXT: 'text',
    HAND: 'hand',
    STICKY_NOTE: 'stickyNote',
    CONNECTOR: 'connector',
    LASER: 'laser',
} as const;

Note: TOOL_IDS.HAND is defined as a constant for tool-switching UI purposes, but there is no HandTool class. The hand/pan behavior (viewport panning via space+drag and wheel zoom) is handled directly by InputPipeline in @hfu.digital/boardkit-react, not through the Tool abstract class.

Creating Custom Tools

Extend the Tool abstract class and register it:

import { Tool, type InputEvent, type ToolResult, type SceneState } from '@hfu.digital/boardkit-core';

class StampTool extends Tool {
    readonly id = 'stamp';
    readonly name = 'Stamp';
    state: ToolState = 'idle';

    onPointerDown(event: InputEvent, scene: SceneState): ToolResult {
        // Create a stamp element at the click position
        return {
            mutations: [{ type: 'create', elementId: 'stamp-1', pageId: '...', data: { /* ... */ }, timestamp: Date.now() }],
            state: 'idle',
        };
    }

    onPointerMove(_event: InputEvent, _scene: SceneState): ToolResult {
        return { state: this.state };
    }

    onPointerUp(_event: InputEvent, _scene: SceneState): ToolResult {
        return { state: 'idle' };
    }

    onCancel(): ToolResult {
        return { state: 'idle' };
    }
}

// Register
const registry = ToolRegistry.createDefault();
registry.register(new StampTool());

On this page