How to Add New Edit Operations to the SuperSplat Editor

To add new edit operations to SuperSplat, implement the EditOp interface in 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. 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, 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) 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) 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). 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 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. 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:

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

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.

Operation Definition

Add this class to src/edit-ops.ts after the existing operation definitions:

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

// 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 Core edit operation definitions (EditOp, StateOp, MultiOp, EntityTransformOp, SelectAllOp)
src/edit-history.ts Serializes, queues, and replays edits for undo/redo (lines 45–73, 81–99)
src/editor.ts Registers editor-wide events and connects EditHistory with the scene
src/events.ts Publish-subscribe event bus wrapper used throughout the codebase
src/ui/color-panel.ts Example of firing edit operations via events.fire('edit.add', op) (lines 290–392)
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 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 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) 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). 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, splat list interactions in 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, 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.

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 →