My App
BoardKitGuides

Custom Tools

How to extend the Tool class and register custom drawing tools

This guide explains how to create custom drawing tools by extending the Tool abstract class from @hfu.digital/boardkit-core.

Tool Lifecycle

Every tool follows a state machine:

idle → active → finishing → idle
  1. idle — Tool waits for input
  2. active — Tool is processing (e.g., user is dragging)
  3. finishing — Tool is completing (e.g., finalizing a shape)

The tool receives pointer events and returns a ToolResult with mutations (permanent changes) and previews (temporary visual feedback).

Step 1: Extend the Tool Class

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

export class StarTool extends Tool {
    readonly id = 'star';
    readonly name = 'Star';
    state: ToolState = 'idle';

    private startPosition: Point | null = null;

    onPointerDown(event: InputEvent, scene: SceneState): ToolResult {
        this.state = 'active';
        this.startPosition = event.position;

        return {
            cursor: 'crosshair',
            state: 'active',
        };
    }

    onPointerMove(event: InputEvent, scene: SceneState): ToolResult {
        if (this.state !== 'active' || !this.startPosition) {
            return { state: this.state };
        }

        // Show a preview of the star while dragging
        const preview = this.createStarElement(
            this.startPosition,
            event.position,
            'preview-star',
        );

        return {
            preview: [preview],
            cursor: 'crosshair',
            state: 'active',
        };
    }

    onPointerUp(event: InputEvent, scene: SceneState): ToolResult {
        if (this.state !== 'active' || !this.startPosition) {
            this.state = 'idle';
            return { state: 'idle' };
        }

        const elementId = `star-${Date.now()}`;
        const element = this.createStarElement(
            this.startPosition,
            event.position,
            elementId,
        );

        // Create a mutation to add the element to the scene
        const mutation: ElementMutation = {
            type: 'create',
            elementId,
            pageId: element.pageId,
            data: element as any,
            timestamp: Date.now(),
        };

        this.state = 'idle';
        this.startPosition = null;

        return {
            mutations: [mutation],
            state: 'idle',
        };
    }

    onCancel(): ToolResult {
        this.state = 'idle';
        this.startPosition = null;
        return { state: 'idle' };
    }

    private createStarElement(start: Point, end: Point, id: string): Element {
        const width = Math.abs(end.x - start.x);
        const height = Math.abs(end.y - start.y);
        const x = Math.min(start.x, end.x);
        const y = Math.min(start.y, end.y);

        return {
            id,
            pageId: '', // Will be set by the store
            type: 'shape',
            zIndex: Date.now(),
            createdBy: '',
            createdAt: new Date().toISOString(),
            updatedAt: new Date().toISOString(),
            data: {
                shapeType: 'rectangle', // Use shape type for rendering
                position: { x, y },
                size: { width, height },
                rotation: 0,
                style: {
                    stroke: { color: '#FFD700', width: 3, opacity: 1, lineCap: 'round', lineJoin: 'round' },
                    fill: { type: 'solid', color: '#FFFF00', opacity: 0.5 },
                },
                bounds: { x, y, width, height },
            },
        } as Element;
    }
}

Step 2: Register the Tool

Backend (ToolRegistry)

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

const registry = ToolRegistry.createDefault();
registry.register(new StarTool());

Frontend (BoardKitProvider)

Since BoardKitProvider creates the ToolRegistry internally, you access it via useBoardKit:

import { useEffect } from 'react';
import { useBoardKit } from '@hfu.digital/boardkit-react';
import { StarTool } from './tools/star-tool';

function RegisterCustomTools() {
    const { toolRegistry } = useBoardKit();

    useEffect(() => {
        toolRegistry.register(new StarTool());
    }, [toolRegistry]);

    return null;
}

// In your app:
<BoardKitProvider config={config}>
    <RegisterCustomTools />
    <Whiteboard />
</BoardKitProvider>

Step 3: Add a Toolbar Button

import { useTool } from '@hfu.digital/boardkit-react';

function CustomToolbar() {
    const { activeTool, setTool } = useTool();

    return (
        <div>
            {/* ... other tools ... */}
            <button
                onClick={() => setTool('star')}
                data-active={activeTool === 'star'}
            >
                Star
            </button>
        </div>
    );
}

Tips

  • Previews are temporary elements rendered on the interactive layer. They are discarded each frame and should be recreated in onPointerMove.
  • Mutations are permanent. Only emit them in onPointerUp (or onPointerDown for click-to-place tools).
  • The cursor field sets the CSS cursor while the tool is active.
  • Use event.modifiers.shift to constrain proportions (e.g., perfect squares or circles).
  • Use event.pressure for pressure-sensitive width variation.

On this page