Back to Tldraw

Shape transforms

apps/docs/content/sdk-features/shape-transforms.mdx

5.2.18.7 KB
Original Source

Shape transforms are operations that manipulate multiple shapes together: grouping, aligning, distributing, stacking, packing, flipping, and rotating. Each operation has a dedicated method on the Editor class.

All transform operations respect the editor's parent-child coordinate system and work correctly with shapes that have different parents or rotations. Shapes connected by arrow bindings move together as clusters, so transforms preserve diagram relationships.

Transform operations

Transform methods accept either shape IDs or shape objects and typically operate on the current selection. Grouping changes the shape hierarchy. The spatial operations (alignment, distribution, stacking, packing, flipping, rotation) reposition shapes without changing their parent relationships.

Grouping and ungrouping

Grouping creates a new group shape that becomes the parent of the selected shapes. The editor calculates a common ancestor for the shapes being grouped and creates the group at the appropriate position in the hierarchy. The grouped shapes keep their visual positions on the page, but their coordinates become relative to the group.

typescript
editor.groupShapes([shape1, shape2, shape3])
editor.ungroupShapes([groupShape])

Ungrouping reverses this process by moving the group's children back to the group's parent and removing the group shape itself. The shapes maintain their page positions but return to using their original parent's coordinate space.

Alignment

Alignment moves shapes so they share a common edge or center line. The editor supports six alignment operations: left, right, top, bottom, center-horizontal, and center-vertical. When aligning shapes, the editor first calculates the common bounding box of all selected shapes, then moves each shape to align with the appropriate edge or center of that common box.

typescript
editor.alignShapes(editor.getSelectedShapeIds(), 'left')
editor.alignShapes([box1, box2], 'center-vertical')

Selected shapes connected by arrow bindings move together as a cluster during alignment. If shapes A and B are connected by an arrow and you align them with shape C, A and B move together as a single unit.

Distribution

Distribution spaces shapes evenly between the outermost shapes in a selection. The editor identifies the first and last shapes based on their positions, then calculates the gap needed to distribute the remaining shapes evenly in the space between them. This can create negative gaps if shapes overlap.

typescript
editor.distributeShapes(editor.getSelectedShapeIds(), 'horizontal')
editor.distributeShapes([box1, box2, box3], 'vertical')

Distribution requires at least three shape clusters. Like alignment, shapes connected by arrows form clusters that move together, so the actual number of moveable units may be less than the number of selected shapes.

Stacking

Stacking arranges shapes in a sequence with consistent gaps between them. Unlike distribution, which spaces shapes within a fixed range, stacking positions each shape relative to the previous one with a specified gap.

typescript
editor.stackShapes(editor.getSelectedShapeIds(), 'horizontal', 16)
editor.stackShapes([box1, box2, box3], 'vertical')

If you don't pass a gap, the editor uses its adjacentShapeMargin option. Pass a gap of 0 for automatic gap detection: the editor analyzes the current spacing between shapes and uses the most common gap, or the average gap if no pattern exists.

Packing

Packing arranges shapes into a compact grid layout using a bin-packing algorithm based on potpack. The editor groups shapes by their parent containers, then arranges each group into an efficient rectangular grid. The packed arrangement is centered on the shapes' original center point, which minimizes how far shapes move.

typescript
editor.packShapes(editor.getSelectedShapeIds(), 8)
editor.packShapes([box1, box2, box3, box4])

Packing is useful for cleaning up scattered shapes. The gap parameter controls the padding between packed shapes and defaults to the editor's adjacentShapeMargin option.

Flipping

Flipping mirrors shapes along either the horizontal or vertical axis. The operation uses the center point of all selected shapes' common bounding box as the flip origin. Shapes are scaled by -1 on the appropriate axis, which inverts their position and visual appearance while maintaining their size.

typescript
editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')
editor.flipShapes([box1, box2], 'vertical')

When flipping groups, the editor automatically includes all children of the group to ensure the entire hierarchy is flipped together. Some shapes may opt out of flipping by returning false from their ShapeUtil's canBeLaidOut method.

Rotation

Rotation spins shapes around a common center point. The editor calculates the collective center of all selected shapes, then rotates each shape both around that center point and around its own center. This produces the expected result where shapes orbit the selection center while also rotating individually.

typescript
editor.rotateShapesBy(editor.getSelectedShapeIds(), Math.PI / 4)

The rotation system maintains a snapshot of the initial shape positions and rotations, which supports smooth incremental updates during drag operations. Shape utilities can respond to rotation events through the onRotateStart, onRotate, and onRotateEnd methods.

Parent coordinate transforms

When a shape has a rotated parent, the editor converts page-space movement deltas back into the parent's local coordinate space before updating the shape's position. Moving a child shape produces the correct visual result regardless of parent rotation or nesting depth.

The conversion process uses the parent's page transform matrix to inverse-rotate the delta vector. For example, if a parent is rotated 45 degrees and you want to move a child 10 pixels to the right in page space, the editor calculates what movement in the parent's coordinate space would produce that page-space result.

typescript
const parent = editor.getShapeParent(shape) // [1]
if (parent) {
	const parentTransform = editor.getShapePageTransform(parent) // [2]
	if (parentTransform) shapeDelta.rot(-parentTransform.rotation()) // [3]
}
  1. Get the shape's parent to check if coordinate conversion is needed
  2. Retrieve the parent's full page transform matrix (position, rotation, scale)
  3. Rotate the movement delta by the negative of the parent's rotation to convert from page space to parent space

Align, distribute, and stack all rely on this conversion: they calculate movement in page space but must apply it in each shape's local space.

Shape clustering via arrow bindings

Several transform operations group shapes into clusters based on arrow bindings. When shapes are connected by arrows, they form a logical unit that should move together during transforms. The editor uses a recursive algorithm to collect all shapes connected through arrow bindings, starting from each selected shape and traversing the binding graph.

The clustering algorithm maintains a visited set to avoid processing shapes multiple times and only includes shapes that were part of the initial selection. This means that if A connects to B via an arrow, but only A is selected, then B will not be included in the transform unless B is also explicitly selected.

Each cluster is treated as a single unit with a common bounding box. When the transform calculates movement for the cluster, it applies that movement to all shapes in the cluster. Their relative positions and arrow relationships stay intact.

Shape utility integration

Shape utilities can control whether their shapes participate in transforms through the canBeLaidOut method. This method receives the transform type and the full list of shapes being transformed, so the utility can make context-aware decisions.

typescript
canBeLaidOut(shape: MyShape, info: TLShapeUtilCanBeLaidOutOpts): boolean {
	// info.type is one of: 'align' | 'distribute' | 'pack' | 'stack' | 'flip' | 'stretch' | 'resize_to_bounds'
	return true
}

For rotation operations, shape utilities can respond to rotation lifecycle events. The editor calls onRotateStart when rotation begins, onRotate for each update, and onRotateEnd when rotation completes. These methods can return shape partials to modify the shape during rotation.

  • Keyboard shortcuts - Customize shortcuts for align, distribute, and other transform operations.
  • Selection UI - Build custom controls that can trigger transform operations on selected shapes.