# Supersplat Transform System Implementation: Transform and SplatsTransformHandler Explained

> Explore the Supersplat transform system. Learn how Transform and SplatsTransformHandler utilize a three-phase lifecycle to efficiently apply matrices to per-point splat selections.

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

---

**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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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

```typescript
// 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

```

```typescript
// 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`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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.