Supersplat Transform System Implementation: Transform and SplatsTransformHandler Explained

The Supersplat transform system separates geometric data from UI interactions through a lightweight Transform class and a GPU-aware SplatsTransformHandler that applies matrices to per-point splat selections via a three-phase event lifecycle.

The transform system in the playcanvas/supersplat editor enables efficient manipulation of 3D Gaussian splats through a clean architecture that keeps CPU logic minimal while pushing heavy matrix work to the GPU. At its core, the system pairs a minimal TRS (position-rotation-scale) container with specialized handlers that react to pivot events and manage undo operations.

The Transform Class: Core TRS Container

Defined in src/transform.ts, the Transform class provides an immutable-friendly data structure that stores geometric state without the overhead of full PlayCanvas entities.

Fields and Construction

Each instance tracks position: Vec3, rotation: Quat, and scale: Vec3 (defaulting to (1,1,1)). The constructor new Transform(position?, rotation?, scale?) optionally initializes these components, creating a lightweight object that can be passed throughout the editor.

Mutation and Comparison Methods

The API provides mutators that copy values rather than mutate in place: set(position?, rotation?, scale?) updates all three components, while copy(other) duplicates values from another transform. For state comparison, use equals(other) for exact equality or equalsApprox(other, epsilon) for fuzzy matching against floating-point drift. The equalsTRS(pos, rot, scl) method compares against raw components directly. A clone() method returns deep copies for safe state snapshots.

This minimal surface—approximately 60 lines of code—keeps the selection-transform logic testable and fast according to the source implementation.

SplatsTransformHandler: GPU-Accelerated Point Manipulation

Located in src/splats-transform-handler.ts, SplatsTransformHandler implements the TransformHandler interface to bridge pivot UI events with GPU-resident matrix data. The handler activates whenever splat.numSelected > 0, managing the transform-palette texture that stores per-point matrices.

Activation and Pivot Placement

When activate() is called, the handler caches the current Splat reference and invokes placePivot() to position the transformation origin at either the selection center or boundCenter. This establishes the coordinate space for subsequent operations.

The Three-Phase Lifecycle

The handler implements a strict event-driven lifecycle spanning pivot.started, pivot.moved, and pivot.ended events:

Start Phase
Lines 86-124 compute the world-to-local and local-to-pivot matrices (mat, localToPivot, worldToLocal). The system allocates new palette entries for each selected point and copies the original transforms into the GPU texture buffer.

Update Phase
During lines 138-155, the handler builds a fresh matrix from the pivot's current TRS, transforms it into the splat's local space using localToPivot and worldToLocal, and writes the resulting matrix into the palette for every selected point. The splat's bounding volume refreshes immediately to reflect the changes.

End Phase
Lines 157-188 compose a SplatsTransformOp containing the final matrix and a Map<number, number> called paletteMap that tracks old-to-new palette index mappings. A PlacePivotOp records the final pivot location. Both operations push onto the undo/redo chain via edit.add. The system optionally triggers splat.updatePositions() to read back GPU data when necessary.

Memory Optimization

To prevent garbage collection during interaction, the handler reuses three scratch objects: mat, mat2, and transform. The paletteMap ensures each selected point receives a unique transform entry without reallocating tracking structures per frame.

System Integration and Registration

The transform system wires together through registerTransformHandlerEvents in src/transform-handler.ts. This registration function instantiates both SplatsTransformHandler (for multi-point editing) and EntityTransformHandler (for whole-object mode), pushing the appropriate handler onto the active stack based on selection state.

This architecture allows seamless switching between editing entire entities and manipulating individual splat points while maintaining a consistent event interface.

Entity-Level Counterpart

For completeness, EntityTransformHandler in src/entity-transform-handler.ts follows the identical TransformHandler contract but operates on the splat's underlying PlayCanvas Entity. It stores a single EntityTransformOp in the undo stack and uses the same Transform class for pivot calculations, ensuring design parity between the two pathways.

Code Examples

// Creating and comparing Transform objects
import { Transform } from './transform';
import { Vec3, Quat } from 'playcanvas';

const t = new Transform();
t.set(new Vec3(1, 2, 3), new Quat().setFromEulerAngles(0, 45, 0), new Vec3(2, 2, 2));

// Equality checking
console.log(t.equalsApprox(t.clone(), 0.0001)); // true
// Event-driven transformation workflow
import { Events } from './events';
import { registerTransformHandlerEvents } from './transform-handler';

const events = new Events();
registerTransformHandlerEvents(events);

// Handler activates automatically when splat points are selected
events.fire('pivot.started', pivotData);
// ... drag operation ...
events.fire('pivot.moved', pivotData);
events.fire('pivot.ended', pivotData);

// Undo the transformation
events.invoke('edit.undo');

Summary

  • Transform class: A minimal TRS container in src/transform.ts providing set(), copy(), clone(), and comparison methods for position, rotation, and scale data.
  • SplatsTransformHandler: Manages GPU-resident per-point matrices through phases defined at lines 86-124 (start), 138-155 (update), and 157-188 (end), updating the transform-palette texture and bounding volumes.
  • Memory efficiency: Reuses mat, mat2, and transform objects while tracking palette indices via a Map to avoid per-frame allocations.
  • Undo integration: Generates SplatsTransformOp and PlacePivotOp operations during pivot.ended, pushing them to the edit history for full revert support.
  • Event architecture: All handlers communicate through the central Events bus, enabling clean separation between UI interactions and geometric transformations.

Frequently Asked Questions

What is the Transform class used for in Supersplat?

The Transform class acts as a lightweight, testable container for position-rotation-scale data. It decouples geometric state from PlayCanvas Entity objects, allowing the editor to manipulate selections without the overhead of full scene graph nodes as implemented in src/transform.ts.

How does SplatsTransformHandler update GPU data?

The handler writes transformation matrices into a transform-palette texture that stores per-point matrices on the GPU. During the pivot.moved phase (lines 138-155), it calculates local-space matrices using localToPivot and worldToLocal, then updates the palette entries for all selected points before refreshing the splat's bounding volume.

What is the difference between SplatsTransformHandler and EntityTransformHandler?

SplatsTransformHandler operates on individual selected points within a splat using GPU palette textures and activates when numSelected > 0. EntityTransformHandler transforms the entire PlayCanvas Entity as a single unit and operates when no specific points are selected. Both implement the same TransformHandler interface for seamless switching.

How does the transform system handle undo operations?

When a transformation ends (pivot.ended), the handler creates a SplatsTransformOp containing the final matrix and paletteMap index mappings, plus a PlacePivotOp recording the pivot position. These operations push onto the undo chain via edit.add (lines 157-188), allowing users to revert or re-apply individual transform actions.

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 →