apps/docs/content/sdk-features/shape-transforms.mdx
Shape transforms are operations that manipulate multiple shapes together: grouping, aligning, distributing, stacking, packing, stretching, 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 with shapes that have different parents or rotations. Shapes connected by arrow bindings move together as clusters.
Transform methods take an array of shape IDs or shape objects; pass editor.getSelectedShapeIds() to operate on the selection. All of them are no-ops when the editor is read-only. Grouping changes the shape hierarchy. The spatial operations reposition shapes without changing their parent relationships.
Editor#groupShapes creates a new group shape that becomes the parent of the given shapes. The editor finds the shapes' common ancestor and creates the group there. The grouped shapes keep their visual positions on the page, but their coordinates become relative to the group.
editor.groupShapes([shape1, shape2, shape3])
editor.groupShapes([shape1, shape2], { groupId: myGroupId, select: false })
editor.ungroupShapes([groupShape])
groupShapes only runs while the select tool is active, and it cancels any in-progress select interaction first. It needs at least two shapes.
Editor#ungroupShapes reverses this: it moves the group's children back to the group's parent and deletes the group shape. The shapes keep their page positions but return to their original parent's coordinate space.
Editor#alignShapes moves shapes so they share a common edge or center line. The six single-axis operations are left, right, top, bottom, center-horizontal, and center-vertical. Use center to align shapes by both their horizontal and vertical centers in one call.
When aligning shapes, the editor first calculates the common bounding box of all selected shapes. It then moves each shape to the appropriate edge or center of that common box.
editor.alignShapes(editor.getSelectedShapeIds(), 'left')
editor.alignShapes([box1, box2], 'center-vertical')
editor.alignShapes([box1, box2], 'center')
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.
Editor#distributeShapes 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.
Editor#stackShapes 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 measures the current spacing between shapes and uses the most common gap, or the average gap if no pattern exists. Automatic detection needs at least three clusters; with fewer, the call does nothing.
Editor#packShapes arranges shapes into a compact grid using a bin-packing algorithm based on potpack. Shapes connected by arrows form clusters, and all clusters are packed into a single grid centered on the shapes' original center point, so shapes move as little as possible.
editor.packShapes(editor.getSelectedShapeIds(), 8)
editor.packShapes([box1, box2, box3, box4])
The gap parameter controls the padding between packed shapes and defaults to the editor's adjacentShapeMargin option.
Editor#flipShapes mirrors shapes along the horizontal or vertical axis. The flip origin is the center of the shapes' common bounding box. Each shape is scaled by -1 on that axis, which inverts its position and appearance while keeping its size.
editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')
editor.flipShapes([box1, box2], 'vertical')
When flipping groups, the editor includes all children of the group so the whole hierarchy flips together. Shapes can opt out of flipping by returning false from ShapeUtil#canBeLaidOut; arrows do this when the shape they're bound to isn't part of the flip.
Editor#rotateShapesBy rotates shapes by a delta in radians around a common center point. The editor calculates the center of all the shapes' rotated bounds, then rotates each shape around that point and around its own origin: shapes orbit the selection center while also rotating individually. Pass { center } to rotate around a different point.
editor.rotateShapesBy(editor.getSelectedShapeIds(), Math.PI / 4)
editor.rotateShapesBy([box1, box2], Math.PI / 2, { center: { x: 0, y: 0 } })
Shape utils can respond to rotation through ShapeUtil#onRotateStart, ShapeUtil#onRotate, and ShapeUtil#onRotateEnd. For a one-off rotateShapesBy call, all three fire in sequence; during an interactive rotate, onRotate fires on every update.
Editor#stretchShapes resizes shapes to fill their common bounding box along one axis. Only shapes whose page rotation is a multiple of 90 degrees participate.
editor.stretchShapes(editor.getSelectedShapeIds(), 'horizontal')
Align, distribute, stack, and pack calculate movement in page space but apply it in each shape's parent space. When a shape's parent is rotated, the editor rotates the page-space delta by the negative of the parent's rotation (from Editor#getShapeParentTransform) before updating the shape's x and y. Moving a child shape produces the correct visual result regardless of parent rotation or nesting depth. See Coordinates.
Align, distribute, stack, and pack group shapes into clusters based on arrow bindings. Starting from each shape, the editor follows arrow bindings recursively and collects every connected shape that is also in the input list. If A connects to B via an arrow but only A is passed in, B is not included.
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 every shape in the cluster, so their relative positions and arrow relationships stay intact.
Shape utils control whether their shapes participate in transforms through ShapeUtil#canBeLaidOut. It receives the transform type and the full list of shapes being transformed, so the util can decide based on context.
canBeLaidOut(shape: MyShape, info: TLShapeUtilCanBeLaidOutOpts): boolean {
// info.type is one of: 'align' | 'distribute' | 'pack' | 'stack' | 'flip' | 'stretch' | 'resize_to_bounds'
return true
}