# How the PlayCanvas Supersplat Tool System Architecture Works: ToolManager and Individual Tools

> Explore the PlayCanvas supersplat tool system architecture. Learn how ToolManager and individual tools use an event-driven system for exclusive activation and simple lifecycle methods.

- Repository: [PlayCanvas/supersplat](https://github.com/playcanvas/supersplat)
- Tags: architecture
- Published: 2026-05-10

---

**Supersplat's editing UI uses a lightweight event-driven architecture centered on a `ToolManager` that maintains exclusive tool activation through a central `Events` bus, with each tool implementing simple `activate`/`deactivate` lifecycle methods.**

The `playcanvas/supersplat` repository implements a modular tool system for 3D Gaussian Splat editing that decouples interaction logic from rendering. This architecture relies on a central event bus to coordinate between the `ToolManager` and individual tools, enabling seamless switching between transform gizmos, selection brushes, and measurement utilities. Understanding this PlayCanvas Supersplat tool system architecture is essential for extending the editor with custom interaction modes.

## Core Event-Driven Architecture

At the foundation lies the `Events` class ([`src/events.ts`](https://github.com/playcanvas/supersplat/blob/main/src/events.ts)), a thin wrapper around PlayCanvas' `EventHandler`. This class enables any module to fire named events and expose synchronous "functions" that other modules can invoke. Unlike direct method calls, this event bus allows tools to remain completely decoupled from each other and from the UI layer.

The architecture follows a pub-sub pattern where:

- **Publishers** fire events like `tool.move` or `selection.changed`
- **Subscribers** listen for these events to update state or trigger actions
- **Functions** provide synchronous query capabilities such as `tool.active` or `tool.coordSpace`

## ToolManager Implementation

The `ToolManager` class ([`src/tools/tool-manager.ts`](https://github.com/playcanvas/supersplat/blob/main/src/tools/tool-manager.ts)) serves as the central coordinator for tool lifecycle management. It maintains an internal registry of tool instances and enforces exclusive activation—only one tool can be active at any time.

### Registration and Activation Flow

When a tool registers with the manager, the system wires an event listener of the form `tool.<name>` that calls `ToolManager.activate(name)`. The activation sequence follows a strict protocol:

1. **Deactivate** the current tool (if any) by calling its `deactivate()` method and broadcasting `tool.<old>.deactivated` and `tool.deactivated`
2. **Activate** the new tool by storing its name, invoking its `activate()` method, and emitting `tool.<new>.activated` and `tool.activated`

This ensures clean state transitions where tools properly release resources (event listeners, SVG overlays, gizmos) before the next tool initializes.

### Coordinate Space Management

The manager tracks the current coordinate space (`'local'` or `'world'`) and exposes it via the synchronous function `tool.coordSpace`. Tools can change this setting through `tool.setCoordSpace` or toggle it with `tool.toggleCoordSpace`. When the space changes, the manager emits `tool.coordSpace`, allowing transform tools to reattach their gizmos accordingly.

## Individual Tool Implementation

All tools conform to a minimal interface requiring only `activate(): void` and `deactivate(): void` methods. They receive the global `Events` instance during instantiation, typically occurring in [`src/main.ts`](https://github.com/playcanvas/supersplat/blob/main/src/main.ts).

### Transform-Based Tools

The `MoveTool`, `RotateTool`, and `ScaleTool` extend the abstract `TransformTool` class ([`src/tools/transform-tool.ts`](https://github.com/playcanvas/supersplat/blob/main/src/tools/transform-tool.ts)). This base class manages:

- **Gizmo creation**: Instantiates `TransformGizmo` and a hidden `pivot` entity that follows the current selection
- **Event forwarding**: Listens to gizmo events (`transform:start`, `transform:move`, `transform:end`) and forwards them to the `Pivot` system
- **Coordinate space adaptation**: Reattaches the gizmo when `tool.coordSpace` changes or when selection changes occur
- **Screen-space sizing**: Adjusts gizmo size on `camera.resize` and `camera.ortho` events

When activated, the tool sets `active = true`, registers listeners for pivot placement and selection changes, and calls `reattach()` to display the gizmo. Deactivation reverses these steps, hiding the gizmo and removing listeners.

### Selection Tools

Tools like `RectSelection`, `BrushSelection`, `LassoSelection`, `FloodSelection`, `PolygonSelection`, `SphereSelection`, `BoxSelection`, and `EyedropperSelection` implement a consistent pattern using SVG overlays or canvas masks:

```typescript
this.activate = () => {
    svg.classList.remove('hidden');
    parent.style.display = 'block';
    parent.addEventListener('pointerdown', this.handlePointerDown);
    // Additional pointer event listeners...
};

this.deactivate = () => {
    svg.classList.add('hidden');
    parent.style.display = 'none';
    parent.removeEventListener('pointerdown', this.handlePointerDown);
    // Cleanup...
};

```

These tools capture pointer events to draw temporary shapes, then fire `select.byMask` with operations (`add`, `remove`, or `set`) upon completion.

### Measure Tool

The `MeasureTool` ([`src/tools/measure-tool.ts`](https://github.com/playcanvas/supersplat/blob/main/src/tools/measure-tool.ts)) demonstrates a complex workflow combining multiple subsystems:

- **Visual elements**: Creates SVG lines and end-caps for visual feedback
- **Gizmo integration**: Uses a `TranslateGizmo` attached to a temporary pivot entity for dragging measurement points
- **UI synchronization**: Displays length via `Label` and `NumericInput` components, updating on `pivot.moved` and UI `change` events
- **Lifecycle management**: Registers under the key `'measure'` and cleans up all visual elements on deactivation

## System Integration and Event Flow

The complete integration flow in [`src/main.ts`](https://github.com/playcanvas/supersplat/blob/main/src/main.ts) illustrates how these components connect:

1. **Initialization**: Creates a single `Events` instance shared across the application
2. **Manager creation**: Instantiates `ToolManager` with the events bus
3. **Tool registration**: Constructs each tool (passing `Events` and dependencies like `Scene` or mask canvas) and registers them via `toolManager.register(name, instance)`
4. **UI binding**: UI buttons fire `tool.<name>` events when clicked
5. **Activation**: `ToolManager.activate(name)` handles the transition, ensuring exclusive access
6. **Forced deactivation**: The `tool.deactivate` event activates "null", cleaning up the previous tool

This design keeps the rendering engine decoupled from interaction logic, allowing tools to inject temporary UI elements (SVG overlays) and 3D gizmos only while active.

## Practical Implementation Examples

### Creating and Registering the ToolManager

```typescript
import { Events } from './events';
import { ToolManager } from './tools/tool-manager';
import { MoveTool } from './tools/move-tool';

// Application initialization
const events = new Events();
const toolManager = new ToolManager(events);

// Register tools
toolManager.register('move', new MoveTool(events, scene));

// UI activation trigger
document.getElementById('moveBtn')!.onclick = () => {
    events.fire('tool.move');
};

// Global deactivation
events.fire('tool.deactivate');

```

*Source:* [`src/main.ts`](https://github.com/playcanvas/supersplat/blob/main/src/main.ts) (lines 23-36) for registration patterns; [`src/tools/tool-manager.ts`](https://github.com/playcanvas/supersplat/blob/main/src/tools/tool-manager.ts) for manager implementation.

### Minimal Custom Tool Template

```typescript
import { Events } from '../events';

export class CustomTool {
    activate!: () => void;
    deactivate!: () => void;

    constructor(events: Events) {
        this.activate = () => {
            console.log('Custom tool activated');
            // Setup UI, listeners, gizmos...
        };
        
        this.deactivate = () => {
            console.log('Custom tool deactivated');
            // Cleanup...
        };
    }
}

// Registration
toolManager.register('custom', new CustomTool(events));

```

*Source pattern:* See [`src/tools/move-tool.ts`](https://github.com/playcanvas/supersplat/blob/main/src/tools/move-tool.ts) or [`src/tools/lasso-selection.ts`](https://github.com/playcanvas/supersplat/blob/main/src/tools/lasso-selection.ts) for complete implementations.

### Monitoring Tool State Changes

```typescript
// Listen for activation events
events.on('tool.activated', (toolName: string) => {
    console.log('Active tool:', toolName);
    // Update UI state, shortcuts...
});

// Query current tool synchronously
const currentTool = events.function('tool.active');
const coordSpace = events.function('tool.coordSpace');

```

*Source:* [`src/tools/tool-manager.ts`](https://github.com/playcanvas/supersplat/blob/main/src/tools/tool-manager.ts) (lines 81-83) for event emission logic.

## Summary

- **Event-Driven Core**: The `Events` class in [`src/events.ts`](https://github.com/playcanvas/supersplat/blob/main/src/events.ts) provides the central bus enabling loose coupling between tools and UI components.
- **Exclusive Activation**: `ToolManager` in [`src/tools/tool-manager.ts`](https://github.com/playcanvas/supersplat/blob/main/src/tools/tool-manager.ts) enforces single-tool-active semantics with proper `activate`/`deactivate` lifecycle management.
- **Transform Tools**: Extend `TransformTool` ([`src/tools/transform-tool.ts`](https://github.com/playcanvas/supersplat/blob/main/src/tools/transform-tool.ts)) to inherit gizmo management, pivot following, and coordinate space adaptation.
- **Selection Tools**: Implement mask-based interaction using SVG overlays or canvas elements, firing `select.byMask` on completion.
- **Coordinate Space**: The manager tracks `local` vs `world` space and notifies tools via the `tool.coordSpace` event.
- **Easy Extension**: New tools require only the two-method interface and registration in [`src/main.ts`](https://github.com/playcanvas/supersplat/blob/main/src/main.ts), following patterns established by `MeasureTool` and `LassoSelection`.

## Frequently Asked Questions

### How does ToolManager prevent multiple tools from being active simultaneously?

The `ToolManager.activate()` method in [`src/tools/tool-manager.ts`](https://github.com/playcanvas/supersplat/blob/main/src/tools/tool-manager.ts) explicitly calls `deactivate()` on the current tool before activating the requested one. It stores the active tool name in an internal variable and broadcasts `tool.deactivated` before emitting `tool.activated`, ensuring that only one tool holds active resources (gizmos, event listeners, SVG elements) at any time.

### What is the difference between tool events and tool functions in the Events system?

**Events** (like `tool.move` or `tool.activated`) are asynchronous broadcasts that trigger actions, while **functions** (like `tool.active` or `tool.coordSpace`) are synchronous queries that return immediate values. The `Events` class wraps PlayCanvas' `EventHandler` to support both patterns, allowing the `ToolManager` to expose read-only state queries that tools and UI components can invoke directly.

### How do transform tools update when the coordinate space changes?

The `TransformTool` base class listens for the `tool.coordSpace` event emitted by `ToolManager`. When this fires, the tool calls its `reattach()` method, which updates the gizmo's transform to match either local or world space coordinates. This happens automatically without requiring manual reactivation of the tool.

### Can I create a tool that doesn't use gizmos or SVG overlays?

Yes. The minimal interface only requires `activate()` and `deactivate()` methods. A tool could manipulate the scene directly, modify selection logic, or interact with other subsystems without creating visual overlays. The `MeasureTool` demonstrates mixed approaches (SVG + gizmo), while selection tools show canvas/SVG patterns, but abstract tools that only process data or modify scene state are equally valid within this architecture.