apps/docs/content/sdk-features/shape-transforms.mdx
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 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 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.
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 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.
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 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.
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 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.
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 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.
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 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.
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 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.
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.
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.
const parent = editor.getShapeParent(shape) // [1]
if (parent) {
const parentTransform = editor.getShapePageTransform(parent) // [2]
if (parentTransform) shapeDelta.rot(-parentTransform.rotation()) // [3]
}
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.
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 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.
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.