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

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), 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) 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.

Transform-Based Tools

The MoveTool, RotateTool, and ScaleTool extend the abstract TransformTool class (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:

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) 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 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

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 (lines 23-36) for registration patterns; src/tools/tool-manager.ts for manager implementation.

Minimal Custom Tool Template

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 or src/tools/lasso-selection.ts for complete implementations.

Monitoring Tool State Changes

// 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 (lines 81-83) for event emission logic.

Summary

  • Event-Driven Core: The Events class in src/events.ts provides the central bus enabling loose coupling between tools and UI components.
  • Exclusive Activation: ToolManager in 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) 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, 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →