# How to Add New Edit Operations to the SuperSplat Editor

> Learn to effortlessly add new edit operations to SuperSplat. Implement the EditOp interface with do and undo methods, then dispatch your operation to seamlessly integrate with the undo/redo system.

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

---

**To add new edit operations to SuperSplat, implement the `EditOp` interface in [`src/edit-ops.ts`](https://github.com/playcanvas/supersplat/blob/main/src/edit-ops.ts) with `do()` and `undo()` methods, then dispatch the operation via `events.fire('edit.add', op)` to automatically integrate with the undo/redo system.**

SuperSplat is a 3D Gaussian splat editor built on PlayCanvas that handles complex scene manipulations through a robust command pattern architecture. When you need to extend the editor with custom functionality—whether renaming splats, modifying selection states, or applying entity transforms—you must work within this operation-based system to maintain full undo/redo support.

## Understanding the Edit Operation Architecture

SuperSplat’s editing system is built around three core concepts that ensure isolation and serializability. Understanding these foundations is essential before implementing custom operations.

### The EditOp Interface

All edit operations must conform to the `EditOp` interface defined in [`src/edit-ops.ts`](https://github.com/playcanvas/supersplat/blob/main/src/edit-ops.ts). This contract requires:

- **`do()`** – Executes the operation and applies changes to the scene
- **`undo()`** – Reverses the operation to restore the previous state
- **`destroy()`** (optional) – Cleans up resources when the operation is removed from history

The `EditHistory` class ([`src/edit-history.ts`](https://github.com/playcanvas/supersplat/blob/main/src/edit-history.ts), lines 45–73 and 81–99) manages these operations through an internal promise chain, ensuring that asynchronous work completes before subsequent edits execute.

### StateOp for Per-Gaussian Manipulations

If your operation manipulates per-Gaussian state—such as selection, lock status, or color values—inherit from `StateOp` instead of implementing `EditOp` directly. This base class provides reusable bit-mask logic for efficiently toggling individual Gaussian properties.

Reference the existing `SelectAllOp` (lines 80–87 in [`src/edit-ops.ts`](https://github.com/playcanvas/supersplat/blob/main/src/edit-ops.ts)) for per-Gaussian operations, or `EntityTransformOp` (lines 64–84) for simple entity-level transformations that implement the interface directly.

### The Event Bus and EditHistory

The editor uses a centralized event system ([`src/events.ts`](https://github.com/playcanvas/supersplat/blob/main/src/events.ts)) for decoupled communication. All edits flow through the `edit.add` event, while `edit.undo` and `edit.redo` handle history navigation (as seen in [`src/ui/bottom-toolbar.ts`](https://github.com/playcanvas/supersplat/blob/main/src/ui/bottom-toolbar.ts)). This architecture prevents race conditions with GPU read-backs and ensures serial execution.

## Step-by-Step Implementation Guide

Follow these specific steps to integrate a new edit operation into the SuperSplat codebase.

### Step 1: Define the Operation Class

Create a new class in [`src/edit-ops.ts`](https://github.com/playcanvas/supersplat/blob/main/src/edit-ops.ts) that implements the `EditOp` interface. Store only the minimal data required to reverse the operation, such as references to affected splats and their previous states.

For operations involving splat metadata (names, visibility), implement the interface directly. For per-Gaussian bit manipulations, extend `StateOp` to leverage existing masking utilities.

### Step 2: Export the Class

Add your new class to the module’s export list at the bottom of [`src/edit-ops.ts`](https://github.com/playcanvas/supersplat/blob/main/src/edit-ops.ts). This ensures other modules—including UI components and tools—can import and instantiate your operation.

### Step 3: Wire Into the UI or Tools

Construct your operation instance wherever the user action occurs, then dispatch it via the event bus:

```typescript
// Example from src/ui/color-panel.ts (lines 290-392)
const op = new YourCustomOp(splat, newValue, oldValue);
events.fire('edit.add', op);

```

This pattern applies to toolbar buttons, menu items, keyboard shortcuts, or custom tools. The UI layer is responsible for gathering initial state (old values) and creating the operation instance, while the event system handles execution.

### Step 4: Automatic Undo/Redo Support

No additional code is required for undo/redo functionality. Once you fire `edit.add`, `EditHistory` automatically:

1. Queues the operation in its internal stack
2. Calls `do()` immediately to execute the action
3. Stores the operation for potential `undo()` calls
4. Handles cleanup via `removeForSplat()` if the target splat is deleted

The system manages async execution via its internal promise chain, ensuring that GPU-dependent operations complete before history state changes.

### Step 5: Combine Multiple Operations with MultiOp

For user actions that require several atomic edits—such as moving a pivot point then locking the splat—wrap individual operations in a `MultiOp` instance (defined at lines 361–383 in [`src/edit-ops.ts`](https://github.com/playcanvas/supersplat/blob/main/src/edit-ops.ts)):

```typescript
const multiOp = new MultiOp([
    new MovePivotOp(splat, newPos, oldPos),
    new LockOp(splat, true)
]);
events.fire('edit.add', multiOp);

```

The `MultiOp` class implements `EditOp` itself, batching sub-operations so they undo/redo as a single unit.

## Complete Working Example

Below is a minimal implementation of a "Rename Splat" operation following the patterns established in [`src/edit-ops.ts`](https://github.com/playcanvas/supersplat/blob/main/src/edit-ops.ts).

### Operation Definition

Add this class to [`src/edit-ops.ts`](https://github.com/playcanvas/supersplat/blob/main/src/edit-ops.ts) after the existing operation definitions:

```typescript
export class RenameSplatOp implements EditOp {
    name = 'renameSplat';
    
    constructor(
        public splat: Splat, 
        private newName: string, 
        private oldName: string
    ) {}

    async do() {
        this.splat.name = this.newName;
        await this.splat.updateMetadata();  // Updates UI/state
    }

    async undo() {
        this.splat.name = this.oldName;
        await this.splat.updateMetadata();
    }
}

```

### UI Integration

Wire this operation into your UI component (e.g., [`src/ui/splat-list.ts`](https://github.com/playcanvas/supersplat/blob/main/src/ui/splat-list.ts)):

```typescript
// Inside the rename handler
const oldName = splat.name;
const newName = userInput.value;
events.fire('edit.add', new RenameSplatOp(splat, newName, oldName));

```

When the user triggers this action, `EditHistory` records the operation, making undo/redo instantly available through the standard editor shortcuts and toolbar buttons.

## Key Source Files Reference

| File | Purpose |
|------|---------|
| [`src/edit-ops.ts`](https://github.com/playcanvas/supersplat/blob/main/src/edit-ops.ts) | Core edit operation definitions (`EditOp`, `StateOp`, `MultiOp`, `EntityTransformOp`, `SelectAllOp`) |
| [`src/edit-history.ts`](https://github.com/playcanvas/supersplat/blob/main/src/edit-history.ts) | Serializes, queues, and replays edits for undo/redo (lines 45–73, 81–99) |
| [`src/editor.ts`](https://github.com/playcanvas/supersplat/blob/main/src/editor.ts) | Registers editor-wide events and connects `EditHistory` with the scene |
| [`src/events.ts`](https://github.com/playcanvas/supersplat/blob/main/src/events.ts) | Publish-subscribe event bus wrapper used throughout the codebase |
| [`src/ui/color-panel.ts`](https://github.com/playcanvas/supersplat/blob/main/src/ui/color-panel.ts) | Example of firing edit operations via `events.fire('edit.add', op)` (lines 290–392) |
| [`src/ui/bottom-toolbar.ts`](https://github.com/playcanvas/supersplat/blob/main/src/ui/bottom-toolbar.ts) | Example of undo/redo event firing (`edit.undo`, `edit.redo`) |

## Summary

- **Implement `EditOp`** – Create classes with `do()` and `undo()` methods in [`src/edit-ops.ts`](https://github.com/playcanvas/supersplat/blob/main/src/edit-ops.ts) to define reversible actions.
- **Use `StateOp` for Gaussian data** – Inherit from `StateOp` when manipulating per-Gaussian flags (selection, lock, color) to reuse bit-mask utilities.
- **Fire `edit.add` events** – Dispatch operations via `events.fire('edit.add', op)` to automatically integrate with `EditHistory` and the undo/redo UI.
- **Leverage `MultiOp`** – Batch atomic operations that should undo/redo as a single unit using the `MultiOp` wrapper class.
- **No manual history management** – `EditHistory` automatically handles serialization, async queuing, and cleanup when splats are deleted.

## Frequently Asked Questions

### What is the difference between `EditOp` and `StateOp` in SuperSplat?

`EditOp` is the base interface that requires `do()` and `undo()` methods for any reversible operation. `StateOp` is a specialized implementation in [`src/edit-ops.ts`](https://github.com/playcanvas/supersplat/blob/main/src/edit-ops.ts) specifically for per-Gaussian manipulations that use bit-masks (like selection or lock states). Use `StateOp` when toggling individual Gaussian properties to reuse existing bit-manipulation logic; use direct `EditOp` implementation for entity-level changes like transforms or metadata updates.

### How does `EditHistory` handle asynchronous operations?

`EditHistory` (in [`src/edit-history.ts`](https://github.com/playcanvas/supersplat/blob/main/src/edit-history.ts)) maintains an internal promise chain that serializes edit execution. When you fire `edit.add`, the history queues the operation and waits for any previous async work (such as GPU read-backs) to complete before calling `do()`. This prevents race conditions and ensures that undo/redo operations execute in the correct order even when operations involve asynchronous metadata updates or data transfers.

### Can I modify multiple splats in a single edit operation?

Yes, but consider using `MultiOp` for atomicity. If you need to affect multiple splats as a single undoable action, create individual operation instances for each splat and wrap them in a `MultiOp` (defined at lines 361–383 in [`src/edit-ops.ts`](https://github.com/playcanvas/supersplat/blob/main/src/edit-ops.ts)). This ensures that undoing the action reverses all sub-operations simultaneously, maintaining consistency in the editor state. Alternatively, design your custom `EditOp` to accept arrays of splats if the logic is tightly coupled.

### Where should I place UI triggers for custom edit operations?

Place UI triggers in the appropriate view component within `src/ui/`. For example, color-related operations belong in [`src/ui/color-panel.ts`](https://github.com/playcanvas/supersplat/blob/main/src/ui/color-panel.ts), splat list interactions in [`src/ui/splat-list.ts`](https://github.com/playcanvas/supersplat/blob/main/src/ui/splat-list.ts), and global transformations in the relevant tool handlers. The UI layer should import your operation class from [`src/edit-ops.ts`](https://github.com/playcanvas/supersplat/blob/main/src/edit-ops.ts), gather necessary state (such as old values for undo), instantiate the operation, and dispatch it via `events.fire('edit.add', op)` to maintain separation between UI logic and editing logic.