Lesson 18 — Align & Distribute
Aligning and distributing nodes is one of the most common layout operations — centring a title on a page, distributing icons evenly across a toolbar, or snapping objects to a grid. This lesson covers the GroupTransform system: a single entry point that handles every alignment and distribution variant through structured data objects rather than dozens of separate command factories.
Automating layout is where scripting delivers some of its most visible results. Instead of manually positioning each node with createTransform and calculating offsets yourself, the applyGroupTransform method handles the math — all you specify is what operation to perform and what reference frame to use. The system separates X and Y axes so you can align left in one dimension while distributing in the other, or leave an axis untouched.
By the end of this lesson you will be able to align nodes to edges and centres, distribute them evenly with consistent spacing, and anchor operations to the spread, page, or specific selection boundaries — all in a single command call.
What this lesson covers
doc.applyGroupTransform(xData, yData, selection)— the single entry point for all align and distribute operationsGroupTransformData— the data object that specifies what to do on one axisGroupTransformType— the operation (align to edge, centre, distribute, space)GroupTransformAnchor— the reference frame (spread, page, first selected, last selected, etc.)- Practical patterns: align left, centre on page, distribute horizontally, space evenly
How it works
Every align and distribute operation in Affinity is a GroupTransform command. You describe what to do on the X axis and what to do on the Y axis separately using GroupTransformData objects, then pass both to doc.applyGroupTransform. Passing null for an axis means "do nothing on this axis".
'use strict';
const { Document } = require('/document.js');
const { GroupTransformData, GroupTransformType, GroupTransformAnchor } = require('/commands.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
// Align selected nodes to their left edges, using the spread as reference
const xData = new GroupTransformData();
xData.type = GroupTransformType.Min; // align to minimum (left) edge
xData.anchor = GroupTransformAnchor.FirstSelected; // relative to the leftmost selected node
doc.applyGroupTransform(xData, null, doc.selection);
}
GroupTransformType — what to do
| Value | Constant | Meaning |
|---|---|---|
| 0 | None | No operation on this axis |
| 1 | Min | Align all to the minimum edge (left / top) |
| 2 | Mid | Align all centres |
| 3 | Max | Align all to the maximum edge (right / bottom) |
| 4 | DistributeMin | Distribute by minimum edges (equal gaps between leading edges) |
| 5 | DistributeMid | Distribute by centres (equal gaps between centres) |
| 6 | DistributeMax | Distribute by maximum edges (equal gaps between trailing edges) |
| 7 | Space | Space evenly (equal gaps between objects, not edges) |
GroupTransformAnchor — reference frame
| Value | Constant | Meaning |
|---|---|---|
| 0 | LogicalEdge | The logical bounding edge of the selection |
| 1 | Spread | The spread (canvas) boundaries |
| 2 | SpreadMargins | The spread's margin guides |
| 3 | FirstSelected | The first (bottom in panel) selected node's bounds |
| 4 | LastSelected | The last (top in panel) selected node's bounds |
| 5 | Page | The current page bounds |
| 6 | PageMargins | The current page margin guides |
Common operations
Align left edges (to leftmost selected node)
// Snippet
const xData = new GroupTransformData();
xData.type = GroupTransformType.Min;
xData.anchor = GroupTransformAnchor.FirstSelected;
doc.applyGroupTransform(xData, null, doc.selection);
Centre horizontally on the spread
// Snippet
const xData = new GroupTransformData();
xData.type = GroupTransformType.Mid;
xData.anchor = GroupTransformAnchor.Spread;
doc.applyGroupTransform(xData, null, doc.selection);
Centre both axes on the page
// Snippet
const xData = new GroupTransformData();
xData.type = GroupTransformType.Mid;
xData.anchor = GroupTransformAnchor.Page;
const yData = new GroupTransformData();
yData.type = GroupTransformType.Mid;
yData.anchor = GroupTransformAnchor.Page;
doc.applyGroupTransform(xData, yData, doc.selection);
Distribute horizontally with equal spacing between objects
// Snippet
const xData = new GroupTransformData();
xData.type = GroupTransformType.Space;
xData.anchor = GroupTransformAnchor.LogicalEdge;
doc.applyGroupTransform(xData, null, doc.selection);
Distribute centres vertically
// Snippet
const yData = new GroupTransformData();
yData.type = GroupTransformType.DistributeMid;
yData.anchor = GroupTransformAnchor.LogicalEdge;
doc.applyGroupTransform(null, yData, doc.selection);
Align right edges to the spread's right margin
// Snippet
const xData = new GroupTransformData();
xData.type = GroupTransformType.Max;
xData.anchor = GroupTransformAnchor.SpreadMargins;
doc.applyGroupTransform(xData, null, doc.selection);
Full example — align & distribute dialog
'use strict';
const { Document } = require('/document.js');
const { GroupTransformData, GroupTransformType, GroupTransformAnchor } = require('/commands.js');
const { Dialog, DialogResult } = require('/dialog.js');
const doc = Document.current;
if (!doc || doc.selection.nodes.length < 2) {
console.log('Select at least 2 nodes first.');
} else {
const dlg = Dialog.create('Align & Distribute');
const col = dlg.addColumn();
const alignGrp = col.addGroup('Align horizontal');
const hAlignCombo = alignGrp.addComboBox('To', [
'Left edges', 'Centres', 'Right edges'
], 0);
const anchorGrp = col.addGroup('Reference');
const anchorCombo = anchorGrp.addComboBox('Anchor', [
'First selected', 'Last selected', 'Spread', 'Page'
], 0);
const distGrp = col.addGroup('Distribute horizontal');
const distCombo = distGrp.addComboBox('Method', [
'None', 'Leading edges', 'Centres', 'Trailing edges', 'Space evenly'
], 0);
if (dlg.runModal() != DialogResult.Ok) { console.log('Cancelled.'); }
else {
const anchorMap = [
GroupTransformAnchor.FirstSelected,
GroupTransformAnchor.LastSelected,
GroupTransformAnchor.Spread,
GroupTransformAnchor.Page
];
const alignTypeMap = [
GroupTransformType.Min,
GroupTransformType.Mid,
GroupTransformType.Max
];
const distTypeMap = [
GroupTransformType.None,
GroupTransformType.DistributeMin,
GroupTransformType.DistributeMid,
GroupTransformType.DistributeMax,
GroupTransformType.Space
];
const anchor = anchorMap[anchorCombo.selectedIndex];
const alignIdx = hAlignCombo.selectedIndex;
const distIdx = distCombo.selectedIndex;
// Apply align
const xAlign = new GroupTransformData();
xAlign.type = alignTypeMap[alignIdx];
xAlign.anchor = anchor;
doc.applyGroupTransform(xAlign, null, doc.selection);
// Apply distribute if selected
if (distIdx > 0) {
const xDist = new GroupTransformData();
xDist.type = distTypeMap[distIdx];
xDist.anchor = GroupTransformAnchor.LogicalEdge;
doc.applyGroupTransform(xDist, null, doc.selection);
}
console.log('Done.');
}
}
Combining align and distribute
Both axes are processed independently and can be combined in a single applyGroupTransform call. For example, snap all selected nodes to a grid: align to the left of the spread on X, distribute evenly on Y:
// Snippet
const xData = new GroupTransformData();
xData.type = GroupTransformType.Min;
xData.anchor = GroupTransformAnchor.Spread;
const yData = new GroupTransformData();
yData.type = GroupTransformType.Space;
yData.anchor = GroupTransformAnchor.LogicalEdge;
doc.applyGroupTransform(xData, yData, doc.selection);
Using DocumentCommand directly
doc.applyGroupTransform is the convenience form. The underlying command is:
// Snippet
const { DocumentCommand } = require('/commands.js');
const cmd = DocumentCommand.createGroupTransform(selection, xData, yData);
doc.executeCommand(cmd);
Use the command form when you need to batch the transform inside a CompoundCommandBuilder alongside other operations.
Key gotchas
Requires at least 2 nodes selected for distribute operations. With only one node selected, Space, DistributeMin, DistributeMid, and DistributeMax are silent no-ops. Min, Mid, and Max work on a single node (they move it to align against the anchor).
GroupTransformData uses new, not a static factory. Unlike most SDK types, GroupTransformData is constructed with new GroupTransformData(). Its type and anchor properties are then set directly.
node.baseBoxInterface.baseBox reflects LOCAL coordinates after transform. The visual positions of nodes change but baseBox values may appear unchanged because the actual translation is encoded in the node's transformInterface. Use node.getSpreadBaseBox(false) to read the post-transform spread-space bounding box if you need to verify results.
Anchor anchors that reference selected nodes (FirstSelected, LastSelected) use panel order, not selection order. First selected in panel terms is the bottommost node in the Layers panel.
Operations are not preview-able via the
previewflag — GroupTransform commands commit immediately.
Exercises
Exercise 18.1 — Centre on canvas
Select any number of nodes. Centre them all horizontally and vertically on the spread in a single operation.
Exercise 18.2 — Distribute evenly
Select 5 or more shapes in a row. Distribute them with equal horizontal spacing between objects. The leftmost and rightmost should stay in place.
Exercise 18.3 — Grid snap
Write a script that aligns all selected nodes into a 3-column grid: the first three align horizontally, the next three at a fixed Y offset, and so on. Hint: iterate selected nodes in groups of 3, using FirstSelected/LastSelected anchors.
Exercise 18.4 — Align dialog
Build a dialog with dropdowns for horizontal align (None / Left / Centre / Right), vertical align (None / Top / Middle / Bottom), and reference (Spread / Page / Selection). Apply the chosen combination in a single command.
SDK modules used in this lesson
/commands—DocumentCommand,GroupTransformData,GroupTransformType,GroupTransformAnchor,CompoundCommandBuilder/document—Document/dialog—Dialog,DialogResult/selections—Selection
See also
Example scripts
18a-align-selection.js
Download 18a-align-selection.js
'use strict';
// Example 17a — Align selection
// Lesson 17: Align & Distribute
//
// Demonstrates the six most common alignment operations using GroupTransform.
// Runs through each in sequence with a 0 delay between them for illustration.
//
// Usage: select 3 or more nodes, then run.
const { Document } = require('/document.js');
const { GroupTransformData, GroupTransformType, GroupTransformAnchor } = require('/commands.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
const count = doc.selection.nodes.length;
if (count < 2) {
console.log('Select at least 2 nodes first.');
} else {
console.log(`Operating on ${count} nodes.`);
function align(xType, xAnchor, yType, yAnchor, label) {
const xData = xType !== null ? Object.assign(new GroupTransformData(), { type: xType, anchor: xAnchor }) : null;
const yData = yType !== null ? Object.assign(new GroupTransformData(), { type: yType, anchor: yAnchor }) : null;
doc.applyGroupTransform(xData, yData, doc.selection);
console.log(label, 'done.');
}
// Centre all nodes on the spread
align(
GroupTransformType.Mid, GroupTransformAnchor.Spread,
GroupTransformType.Mid, GroupTransformAnchor.Spread,
'Centre on spread'
);
// Now distribute them horizontally with equal spacing
align(
GroupTransformType.Space, GroupTransformAnchor.LogicalEdge,
null, null,
'Space evenly (horizontal)'
);
// Align tops to the topmost selected node
align(
null, null,
GroupTransformType.Min, GroupTransformAnchor.FirstSelected,
'Align tops'
);
console.log('All operations complete.');
}
}