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- idle — Tool waits for input
- active — Tool is processing (e.g., user is dragging)
- 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(oronPointerDownfor click-to-place tools). - The
cursorfield sets the CSS cursor while the tool is active. - Use
event.modifiers.shiftto constrain proportions (e.g., perfect squares or circles). - Use
event.pressurefor pressure-sensitive width variation.