Lesson 06 — Geometry, Transforms & Curves
This lesson covers the geometric side of the SDK — how to move, scale, and rotate nodes using transforms, how to draw vector paths programmatically with CurveBuilder, and how to read a node's bounding box. It also covers the coordinate system all SDK measurements use.
Geometry operations unlock a whole class of scripts that go beyond simple property changes: grid layouts, pattern generators, custom shape creation, alignment tools, and anything that needs to know where things are on the canvas. The Transform class and CurveBuilder are the two workhorses here — once you are comfortable with them you can build almost any spatial manipulation imaginable.
What this lesson covers
- Adding basic shapes — rectangles, ellipses, polygons, stars, triangles
- Shape parameters — corner radius, number of sides, star inner radius
- The full parametric shape inventory (cogs, spirals, hearts, callouts…)
- QR codes and their payload (the data the code encodes) types
Transform— moving, scaling, rotating nodesCurveBuilder— programmatically drawing vector paths, beziers and arcsPolyCurve— multi-subpath curves- Unit conversions (document pixels vs points)
- Duplicating nodes in a grid layout
- Converting shapes to curves and rasterising objects
- Knife cut and scissor cut
- Boolean operations and dividing shapes
- Reading back a boolean compound operation
- Adding artboards
TransformBuilderand combining bounding boxes
Coordinate system
The SDK works in document pixels at 96dpi. All coordinates and sizes are in this unit.
To convert from points (as shown in the UI):
// Snippet — assumes doc is already defined (e.g. const doc = Document.current)
const PTS_TO_PX = doc.dpi / 72;
const tenPtInPx = 10 * PTS_TO_PX;
Transforms — moving and resizing nodes
Build a Transform via static factories like createTranslate, then pass it to DocumentCommand.createTransform().
'use strict';
const { Document } = require('/document.js');
const { DocumentCommand } = require('/commands.js');
const { Transform } = require('/geometry.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else if (doc.selection.nodes.isEmpty) { console.log('Select something first.'); }
else {
const sel = doc.selection;
// Pattern 1: instance method — createIdentity, then mutate
const xf = Transform.createIdentity();
xf.translate(100, 50); // translate(x, y) — mutates in place
doc.executeCommand(DocumentCommand.createTransform(sel, xf));
// Pattern 2: static constructor (most concise)
const xf2 = Transform.createTranslate(100, 50);
doc.executeCommand(DocumentCommand.createTransform(sel, xf2));
console.log('Nodes translated.');
}
Transform properties and chainable methods
Access matrix components via .xAxis, .yAxis, .origin, and chain mutating calls like .translate(), .rotate(), .scale().
// Snippet — assumes xf is a Transform
xf.xAxis; // Vector — first column of the matrix
xf.yAxis; // Vector — second column
xf.origin; // Point — translation component
xf.inverted; // Transform — non-mutating inverse
// Chainable mutating methods
xf.translate(50, 30)
.rotate(Math.PI / 4)
.scale(2, 1.5);
// Non-mutating variants return a new Transform
const scaled = xf.scaled(2); // new Transform, xf unchanged
const rotated = xf.rotated(Math.PI / 4);
const combined = xf.translated(50, 0);
// around(originX, originY) — transform about a point, then translate back.
xf.around(100, 100).rotate(Math.PI / 4);
// Compose with another transform
const a = Transform.createTranslate(100, 0);
const b = Transform.createRotate(Math.PI / 4);
const c = a.multiply(b); // a * b — new Transform
a.postmultiplyBy(b); // a = a * b — mutate a in place
a.premultiplyBy(b); // a = b * a — mutate a in place
// Static multiply
const d = Transform.multiply(a, b); // same as a.multiply(b)
// Decompose into components
const data = xf.decompose();
console.log(data.translation, data.rotation, data.scaling, data.shear);
Adding basic shapes
Shapes are created using static .create() factory methods from the /shapes module — never with new. Each shape type has its own class: ShapeRectangle, ShapeEllipse, ShapePolygon, ShapeTriangle, ShapeStar, and more.
The pattern is always the same: create the shape, wrap it in a ShapeNodeDefinition, set its bounding rectangle, then add it to the document.
'use strict';
const { Document } = require('/document.js');
const { ShapeNodeDefinition } = require('/nodes.js');
const { AddChildNodesCommandBuilder } = require('/commands.js');
const { ShapeRectangle, ShapeEllipse, ShapePolygon, ShapeTriangle, ShapeStar } = require('/shapes.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
function addShape(shape, x, y, w, h) {
const def = ShapeNodeDefinition.createDefault();
def.shape = shape;
def.setBoundingRectangle({ x, y, width: w, height: h });
const builder = AddChildNodesCommandBuilder.create();
builder.addShapeNode(def);
doc.executeCommand(builder.createCommand(true));
}
addShape(ShapeRectangle.create(), 100, 100, 200, 150); // rectangle
addShape(ShapeEllipse.create(), 350, 100, 200, 150); // ellipse
addShape(ShapePolygon.create(), 600, 100, 200, 200); // hexagon (default 6 sides)
addShape(ShapeTriangle.create(), 100, 300, 200, 200); // triangle
addShape(ShapeStar.create(), 350, 300, 200, 200); // star
console.log('Shapes added.');
}
ShapeRectangle.create()notnew ShapeRectangle(). All shape classes require a native handle — instantiating withnewthrowsInvalid handle. Always use the static.create()factory method.
Shape parameters
Once a shape is on the canvas, its parameters can be modified using DocumentCommand. Select the shape first or use a per-node selection:
'use strict';
const { Document } = require('/document.js');
const { DocumentCommand } = require('/commands.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else if (doc.selection.nodes.isEmpty) { console.log('Select a shape first.'); }
else {
const sel = doc.selection;
// Corner radius on a rectangle (0 = sharp corners)
doc.executeCommand(DocumentCommand.createSetShapeFloatParam(sel, 'CornerRadius', 20));
// Number of sides on a polygon (integer param)
doc.executeCommand(DocumentCommand.createSetShapeIntParam(sel, 'NumberOfPoints', 3));
// Star inner radius — controls how pointy the star is (0.0–1.0)
doc.executeCommand(DocumentCommand.createSetShapeFloatParam(sel, 'InnerRadius', 0.3));
}
| Type | Params |
|---|---|
| Float | CornerRadius, InnerRadius, sweep, startAngle |
| Int | NumberOfPoints |
Parametric shapes — the full inventory
/shapes exposes 24 shape classes, far beyond the five basics above. Each is created with its static .create() factory and configured through properties before being wrapped in a ShapeNodeDefinition:
| Class | Tunable properties |
|---|---|
ShapeArrow | leftEnd, rightEnd, thickness |
ShapeCalloutEllipse / ShapeCalloutRectangle | tail, absoluteSizes, corner radius (rectangle) |
ShapeCat … ShapeCat4 | (no parameters) |
ShapeCloud | bubbles, innerRadius |
ShapeCog | teeth, innerRadius, holeRadius, toothSize, notchSize, curvature |
ShapeCrescent | leftArc, rightArc |
ShapeDiamond / ShapeTriangle | position |
ShapeDoubleStar | points, innerRadius, pointRadius |
ShapeEllipse / ShapeRectangle | rectangle corners via topLeft…bottomRight |
ShapeHeart | spread |
ShapePie | startAngle, endAngle, sweep, innerRadius, closePie() |
ShapePolygon | sides, smoothPoints |
ShapeSegment | angle, lowerLine, upperLine |
ShapeSpiral | turns, partTurn, decay, style, cusped |
ShapeSquareStar | sides, cutout |
ShapeStar | points, innerRadius, curvedEdges |
ShapeTear | ball, curve, bend, tailPosition |
ShapeTrapezoid | left, right (via setPositions) |
ShapeQRCode | payload — see below |
The generic factory Shape.create(ShapeType.X) also works and returns the correctly-typed subclass — useful when the shape type is chosen at runtime.
Worked property values for the most common shapes:
// Snippet — assumes the Shape* classes are imported from /shapes.js
const star = ShapeStar.create();
star.innerRadius = 0.4; // 0–1 as fraction of outer
star.points = 5;
star.leftCurve = 0.5;
star.rightCurve = 0.5;
const rect = ShapeRectangle.create();
rect.topLeft.setRadius(10, 200, 200);
rect.topRight.setRadius(10, 200, 200);
rect.useSingleRadius = false; // true = all corners equal
const arrow = ShapeArrow.create();
arrow.leftEnd = ShapeArrowEndStyle.None;
arrow.rightEnd = ShapeArrowEndStyle.Arrow;
arrow.thickness = 0.3;
const pie = ShapePie.create();
pie.innerRadius = 0.2;
pie.startAngle = 0;
pie.sweep = 270; // degrees
pie.isClosed = true;
'use strict';
const { Document } = require('/document.js');
const { ShapeNodeDefinition } = require('/nodes.js');
const { AddChildNodesCommandBuilder } = require('/commands.js');
const { ShapeCog, ShapeHeart, ShapeSpiral, ShapeQRCode, QRPayloadURL } = require('/shapes.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
function addShape(shape, x, y, w, h, name) {
const def = ShapeNodeDefinition.createDefault();
def.shape = shape;
def.userDescription = name;
def.setBoundingRectangle({ x, y, width: w, height: h });
const builder = AddChildNodesCommandBuilder.create();
builder.addShapeNode(def);
doc.executeCommand(builder.createCommand(true));
}
const cog = ShapeCog.create();
cog.teeth = 12;
addShape(cog, 100, 100, 150, 150, 'Cog');
const heart = ShapeHeart.create();
heart.spread = 0.6;
addShape(heart, 300, 100, 150, 150, 'Heart');
const spiral = ShapeSpiral.create();
spiral.turns = 4;
addShape(spiral, 500, 100, 150, 150, 'Spiral');
console.log('Shapes added.');
}
Set shape properties before adding the node. Property changes on a shape object are applied when the node is created; changing them afterwards requires
createSetShape*Paramcommands (see Shape parameters).
QR codes
ShapeQRCode is a parametric shape like any other — the difference is its payload, a QRPayload subclass describing what the code encodes. Ten payload types ship with the SDK: QRPayloadURL, QRPayloadWifi, QRPayloadVCard, QRPayloadEmail, QRPayloadPhone, QRPayloadSMS, QRPayloadText, QRPayloadLocation, QRPayloadFaceTime, and QRPayloadWhatsApp.
'use strict';
const { Document } = require('/document.js');
const { ShapeNodeDefinition } = require('/nodes.js');
const { AddChildNodesCommandBuilder } = require('/commands.js');
const { ShapeQRCode, QRPayloadURL, QRPayloadWifi, WifiEncryptionType } = require('/shapes.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
function addQR(payload, x, y, name) {
const qr = ShapeQRCode.create();
qr.payload = payload;
const def = ShapeNodeDefinition.createDefault();
def.shape = qr;
def.userDescription = name;
def.setBoundingRectangle({ x, y, width: 150, height: 150 });
const builder = AddChildNodesCommandBuilder.create();
builder.addShapeNode(def);
doc.executeCommand(builder.createCommand(true));
}
const url = QRPayloadURL.create();
url.url = 'https://example.com';
addQR(url, 100, 300, 'Website QR');
const wifi = QRPayloadWifi.create();
wifi.ssid = 'Studio-5G';
wifi.password = 'hunter2';
wifi.encryption = WifiEncryptionType.WPA2;
addQR(wifi, 300, 300, 'Wifi QR');
console.log('QR codes added.');
}
QRPayload.parse(payloadString) decodes a payload string back into a typed object, and isPayloadValid reports whether the fields are complete enough to encode. Payload data is baked into the node — changing the URL later means replacing the payload and re-applying via shapeInterface.setShape().
QR payload types
| Class | Factory | Key fields |
|---|---|---|
| URL | QRPayloadURL.create(url) | url |
| VCard | QRPayloadVCard.create() | name, phone, email, company, address, birthday, etc. |
QRPayloadEmail.create(address, subject, body) | address, subject, body | |
| SMS | QRPayloadSMS.create(number, content) | number, content |
| Phone | QRPayloadPhone.create(number) | number |
| WiFi | QRPayloadWifi.create(ssid, password, encryption) | ssid, password, encryption (open/wpa2/wpa3) |
| Location | QRPayloadLocation.create(lat, long, elevation) | latitude, longitude, elevation |
| FaceTime | QRPayloadFaceTime.create(recipient) | recipient |
QRPayloadWhatsApp.create(number, content) | number, content | |
| Text | QRPayloadText.create(text) | text |
QRPayloadType identifies the payload kind (payload.payloadType; also accepted by QRPayload.create(type)). ShapeMajorAxis picks a shape's major axis (shape.majorAxis); ShapeSpiralStyle selects the spiral rendering style.
VCard fields
// Snippet — assumes QRPayloadVCard is available
const vcard = QRPayloadVCard.create();
vcard.name = 'Jane Doe';
vcard.firstName = 'Jane';
vcard.lastName = 'Doe';
vcard.email = 'jane@example.com';
vcard.phone = '+1 555 0100';
vcard.company = 'Acme Corp';
vcard.title = 'Engineer';
vcard.address = '123 Main St, City';
vcard.city = 'City';
vcard.postcode = '12345';
vcard.country = 'Country';
vcard.birthday = '1990-01-15';
Building curves with CurveBuilder
Use CurveBuilder to draw vector paths programmatically — beginXY(), lineToXY(), then createCurve().
'use strict';
const { Document } = require('/document.js');
const { CurveBuilder, PolyCurve } = require('/geometry.js');
const { AddChildNodesCommandBuilder } = require('/commands.js');
const { PolyCurveNodeDefinition } = require('/nodes.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
// Build a triangle
const builder = CurveBuilder.create();
builder.beginXY(300, 100); // NOT begin({x,y}) — use beginXY
builder.lineToXY(500, 300); // NOT addLineXY — use lineToXY
builder.lineToXY(100, 300);
builder.close();
const curve = builder.createCurve();
const poly = PolyCurve.create();
poly.addCurve(curve);
// Use createDefault() not create() — create() requires arguments
const nodeDef = PolyCurveNodeDefinition.createDefault();
nodeDef.polyCurve = poly;
const addBuilder = AddChildNodesCommandBuilder.create();
addBuilder.addPolyCurveNode(nodeDef);
doc.executeCommand(addBuilder.createCommand(true));
console.log('Triangle added.');
}
Bezier curves
For curved segments, use addBezierXY(cp1x, cp1y, cp2x, cp2y, endX, endY).
'use strict';
const { Document } = require('/document.js');
const { CurveBuilder, PolyCurve } = require('/geometry.js');
const { AddChildNodesCommandBuilder } = require('/commands.js');
const { PolyCurveNodeDefinition } = require('/nodes.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
const builder = CurveBuilder.create();
builder.beginXY(100, 200);
builder.addBezierXY(
150, 100, // control point 1
250, 100, // control point 2
300, 200 // end point
);
builder.close();
const curve = builder.createCurve();
const poly = PolyCurve.create();
poly.addCurve(curve);
const nodeDef = PolyCurveNodeDefinition.createDefault();
nodeDef.polyCurve = poly;
const addBuilder = AddChildNodesCommandBuilder.create();
addBuilder.addPolyCurveNode(nodeDef);
doc.executeCommand(addBuilder.createCommand(true));
console.log('Bezier curve added.');
}
Bulge / versine arcs
bulgeToXY(x, y, bulge) appends an arc segment described by its bulge factor (positive bulges one way, negative the other — the sweep angle is 4·atan(bulge)). versineTo(end, versine) is the equivalent sagitta-based form (sagitta = perpendicular distance from the chord midpoint to the arc). Both forms are supported.
// Snippet — assumes builder already begun with beginXY
builder.bulgeToXY(100, 0, 0.5); // 90° arc, bulge 0.5
builder.versineTo({x:200, y:0}, 50); // arc with 50px sagitta
Grid duplicator
A common pattern — take a shape and tile it into a grid:
'use strict';
const { Document } = require('/document.js');
const { DocumentCommand, CompoundCommandBuilder } = require('/commands.js');
const { Transform } = require('/geometry.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
const sel = doc.selection;
const node = sel.nodes.first;
if (!node) { console.log('Select one shape.'); }
else {
// baseBoxInterface.baseBox — NOT transformInterface.baseBox (that is undefined)
const bbox = node.baseBoxInterface?.baseBox;
if (!bbox) { console.log('Could not get bounding box.'); }
else {
const cols = 4, rows = 4;
const gapX = 20, gapY = 20;
const stepX = bbox.width + gapX;
const stepY = bbox.height + gapY;
const compound = CompoundCommandBuilder.create();
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
if (row === 0 && col === 0) continue; // skip the original
const xf = Transform.createTranslate(col * stepX, row * stepY);
compound.addCommand(
DocumentCommand.createTransform(sel, xf, { duplicateNodes: true }));
}
}
doc.executeCommand(compound.createCommand());
console.log(`Created ${rows * cols - 1} copies.`);
}
}
}
Converting a shape to curves
Turn a parametric shape (rectangle, ellipse, etc.) into raw editable vector curves.
// Snippet
// Via DocumentCommand
doc.executeCommand(DocumentCommand.createConvertToCurves(sel));
// Or via doc convenience method
doc.convertToCurves(sel);
Rasterising objects
Convert vector layers into pixel rasters, optionally clipping to spread bounds.
// Snippet
// Convert vector to raster pixels
doc.rasteriseObjects(sel);
// rasteriseContentsOnly = true — rasterise contents but keep layer structure
// clipToSpread = true — clip to spread bounds
doc.rasteriseObjects(sel, false, true);
Adding artboards
Use doc.addRectangularArtboard(rect) to add a new artboard at a given position and size.
'use strict';
const { Document } = require('/document.js');
const doc = Document.current;
if (doc) {
// Add a rectangular artboard at a specific position
doc.addRectangularArtboard({ x: 0, y: 0, width: 1920, height: 1080 });
console.log('Artboard added.');
}
Knife cut and scissor cut
Split nodes along a curve (knife) or at a parameterised point (scissor).
// Snippet — assumes doc, sel, and cutCurve are already defined
// Knife cut along a polyCurve
doc.executeCommand(DocumentCommand.createKnifeCut(cutCurve, sel));
// Scissor — split a curve at a specific point
doc.executeCommand(DocumentCommand.createScissorCut(node, polyCurveIndex, curveIndex, true, 0.5));
Boolean operations
Combine or divide overlapping vector nodes. Union merges into one shape, subtract cuts the top shape out of the one below, intersect keeps only the overlap, and xor keeps everything except the overlap. divideShapes splits the selection into separate pieces along every intersection.
'use strict';
const { Document } = require('/document.js');
const { DocumentCommand } = require('/commands.js');
const doc = Document.current;
if (!doc || doc.selection.nodes.length < 2) {
console.log('Select 2+ overlapping vector nodes first.');
} else {
const sel = doc.selection;
// Merge into a single shape
doc.executeCommand(DocumentCommand.createBoolOpUnion(sel));
// Or via doc convenience methods
// doc.boolOpUnion(); doc.boolOpSubtract();
// doc.boolOpIntersect(); doc.boolOpXor(); doc.divideShapes();
console.log('Boolean operation applied.');
}
Boolean ops need 2+ overlapping vector nodes. They operate on the current selection; a single node is a no-op. The result is a new merged/trimmed curve.
Reading back a boolean result
A node produced by a boolean operation keeps a reference to the operation it came from. Read it via node.compoundOperationInterface.compoundOperation — useful for detecting which layers are live boolean groups before destructive edits like convertToCurves:
// Snippet — assumes node is a vector node
const op = node.compoundOperationInterface?.compoundOperation;
if (op) console.log('Live boolean compound:', op);
TransformBuilder
A fluent builder that accumulates transforms in reading order:
// Snippet
const { TransformBuilder } = require('/geometry.js');
const tb = new TransformBuilder();
tb.translateXY(100, 50) // move right 100, down 50
.rotate(Math.PI / 4) // rotate 45°
.scaleXY(2, 1.5); // scale 2x horizontally, 1.5x vertically
const finalXf = tb.transform; // built transform
Each builder method appends to the accumulated transform. Use .transform to retrieve the result.
Key gotchas
node.baseBoxInterface?.baseBox not node.transformInterface?.baseBox. transformInterface.baseBox returns undefined — the bounding box (x, y, width, height) lives on baseBoxInterface.baseBox. transformInterface provides the transform matrix, not the box.
xf.translate(x, y) not xf.translateBy(x, y). translateBy does not exist — the correct instance method is translate. Prefer the static constructors: Transform.createTranslate(x, y), Transform.createRotate(angle), Transform.createScale(x, y). Use Transform.createIdentity() to get a clean transform — makeIdentity() is safe but less idiomatic.
builder.lineToXY not builder.addLineXY. The method to draw a straight line segment is lineToXY(x, y). Similarly, beginXY(x, y) not begin({x, y}) — the XY variant takes two numbers, not an object.
createSetCurves replaces the entire polycurve. Build the full replacement, including any subpaths you want to keep.
Geometry types are proper wrappers. Rectangle, Point, Vector, Size, and similar geometry structs are returned as documented types — not anonymous objects. All expected properties (.x, .y, .width, .height, etc.) behave identically.
All coordinates are in document pixels at 96dpi. The UI shows points — convert with
pts * (doc.dpi / 72).
duplicateNodes: trueincreateTransform— without this, the transform moves the original; with it, it clones and moves the clone.
Exercises
Exercise 6.1 — Shape creator
Write a script that creates a regular hexagon at a given position using CurveBuilder.
Exercise 6.2 — Grid tiler
Select one shape. Generate a 5×3 grid of copies with 10px gaps. Use a compound command.
Exercise 6.3 — Corner rounder
For every selected rectangle, set its corner radius to 12px using setShapeFloatParam.
Exercise 6.4 — Spiral
Use CurveBuilder and bezier segments to draw an approximate Archimedean spiral.
Exercise 6.5 — Wifi QR generator
Build a dialog asking for a network name, password, and encryption type, then create a ShapeQRCode with a matching QRPayloadWifi payload.
Combining bounding boxes
The /geometry module exports unionRects — combines two Rectangle objects into their union:
'use strict';
const { Document } = require('/document.js');
const { unionRects } = require('/geometry.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else if (doc.selection.nodes.isEmpty) { console.log('Select at least one node.'); }
else {
let combined = null;
for (const node of doc.selection.nodes) {
const box = node.getSpreadBaseBox();
combined = combined ? unionRects(combined, box) : box;
}
if (combined) {
console.log('Combined bounds:',
Math.round(combined.x), Math.round(combined.y),
Math.round(combined.width), 'x', Math.round(combined.height));
}
}
Polygons and rectangle helpers
Polygon (from /geometry.js — not to be confused with ShapePolygon) is a raw point loop: Polygon.create(), createLine(p1, p2), createLineXY(x1, y1, x2, y2), createTriangle(..), createRectangle(rect). Inspect with isEmpty / isClosed / isRectangle / isClockwise / pointCount, close and reverse winding with close() / reverse(), and reset with clear(). PolyPolyCurve is the multi-polycurve container for compound paths.
Beside unionRects, the module exports the rest of the overlap toolkit: intersectRects(rc1, rc2) returns the overlap rectangle, pointInRect(rc, pt) is the hit test, and intersectRanges / unionRanges / rangesIntersect are the 1-D numeric-range equivalents.
SDK modules used in this lesson
/shapes—ShapeRectangle,ShapeEllipse,ShapePolygon,ShapeTriangle,ShapeStar,ShapeCog,ShapeHeart,ShapeSpiral,ShapeQRCode,QRPayloadURL,QRPayloadWifi,ShapeType, and all other shape classes/geometry—Transform,TransformBuilder,CurveBuilder,PolyCurve,Rectangle,unionRects,CubicBezier,Vector,Point,Size/commands—DocumentCommand,AddChildNodesCommandBuilder,CompoundCommandBuilder/nodes—PolyCurveNodeDefinition
Further examples
| Example | What it shows |
|---|---|
addPoints.js | Subdividing curves with CurveBuilder, midpoint insertion |
bulgedPolyline.js | CurveBuilder.bulgeToXY, building a polyline with bulge segments |
bulgeVersinePlayground.js | Interactive bulge/versine explorer, bulgeTo, versineTo |
textOnCurvesAndShapes.js | The four curve- and shape-backed text node types |
cornerEffects.js | Corner effects on curves: rounded, bevel, inset, fancy |
divideLength.js | Divide every curve segment into equal lengths |
pathEffects.js | Punk, bloat, twirl and retract path effects |
roundAnyCorner.js | Round corners of selected curves with radius clamping |
makeGrid.js | Grid duplication with Transform and AddChildNodesCommandBuilder |
stepAndRepeat.js | Progressive duplicate/offset/rotate/scale with Transform |
randomise.js | Randomise position, rotation, scale and opacity |
swapObjects.js | Swap positions of two selected objects via Transform |
See also
Example scripts
06a-grid-duplicator.js
Download 06a-grid-duplicator.js
'use strict';
// Example 06a — Grid duplicator
// Lesson 06: Geometry
//
// Takes the selected shape and creates a grid of copies.
// Select ONE shape before running.
const { Document } = require('/document.js');
const { DocumentCommand, CompoundCommandBuilder } = require('/commands.js');
const { Transform } = require('/geometry.js');
const COLS = 4;
const ROWS = 3;
const GAP = 16; // pixels between copies
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
const sel = doc.selection;
const node = sel.nodes.first;
if (!node) {
console.log('Select exactly one shape and run again.');
} else {
// baseBoxInterface.baseBox — NOT transformInterface.baseBox (that is undefined)
const bbox = node.baseBoxInterface?.baseBox;
if (!bbox) { console.log('Could not get bounding box.'); }
else {
const stepX = bbox.width + GAP;
const stepY = bbox.height + GAP;
const compound = CompoundCommandBuilder.create();
let count = 0;
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
if (row === 0 && col === 0) continue; // skip original position
const xf = Transform.createTranslate(col * stepX, row * stepY);
compound.addCommand(
DocumentCommand.createTransform(sel, xf, { duplicateNodes: true })
);
count++;
}
}
doc.executeCommand(compound.createCommand());
console.log(`Grid created: ${COLS}×${ROWS} (${count} copies added).`);
}
}
}
06b-basic-shapes.js
'use strict';
// Example 06b — Basic shapes
// Lesson 06: Geometry, Transforms & Curves
//
// Creates a rectangle, ellipse, polygon, triangle, and star on the current
// document using ShapeNodeDefinition and the shape factory methods.
//
// Key rule: always use ShapeRectangle.create() — never new ShapeRectangle().
// The static factory provides the native handle required by the SDK.
const { Document } = require('/document.js');
const { ShapeNodeDefinition } = require('/nodes.js');
const { AddChildNodesCommandBuilder, DocumentCommand } = require('/commands.js');
const {
ShapeRectangle,
ShapeEllipse,
ShapePolygon,
ShapeTriangle,
ShapeStar
} = require('/shapes.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
// Helper — create and add a shape at a given position and size
function addShape(shape, x, y, w, h, label) {
const def = ShapeNodeDefinition.createDefault();
def.shape = shape;
def.setBoundingRectangle({ x, y, width: w, height: h });
const builder = AddChildNodesCommandBuilder.create();
builder.addShapeNode(def);
const cmd = builder.createCommand(true);
doc.executeCommand(cmd);
const node = cmd.newNodes?.[0];
if (node) node.lineWeightPts = 1;
console.log(`${label} added at (${x}, ${y}) ${w}×${h}`);
return cmd.newNodes?.[0];
}
const W = 180; // shape width
const H = 150; // shape height
const GAP = 20; // gap between shapes
// Row 1 — basic shapes
const rect = addShape(ShapeRectangle.create(), 100, 100, W, H, 'Rectangle');
const ellipse = addShape(ShapeEllipse.create(), 100 + W + GAP, 100, W, H, 'Ellipse');
const polygon = addShape(ShapePolygon.create(), 100 + (W+GAP)*2, 100, W, H, 'Polygon');
// Row 2
const triangle = addShape(ShapeTriangle.create(), 100, 100 + H + GAP, W, H, 'Triangle');
const star = addShape(ShapeStar.create(), 100 + W + GAP, 100 + H + GAP, W, H, 'Star');
// Customise the polygon to a triangle (3 sides) using shape params
if (polygon) {
const sel = doc.selection; // polygon is selected after being added
doc.executeCommand(DocumentCommand.createSetShapeIntParam(sel, 'NumberOfPoints', 3));
console.log('Polygon set to 3 sides');
}
// Add a rounded rectangle by setting corner radius on the rect
if (rect) {
// Re-add a fresh rectangle at a new position with rounded corners
const rounded = addShape(ShapeRectangle.create(), 100 + (W+GAP)*2, 100 + H + GAP, W, H, 'Rounded rectangle');
if (rounded) {
const sel = doc.selection;
doc.executeCommand(DocumentCommand.createSetShapeFloatParam(sel, 'CornerRadius', 25));
console.log('Corner radius set to 25px');
}
}
console.log('Done.');
}
06c-shapes-and-qr.js
'use strict';
// Example 06c — Shapes and QR codes
// Lesson 06: Geometry
//
// Creates parametric shapes beyond the basics (cog, heart, spiral) and
// a QR code with a URL payload, then reports each node's shape type.
const { Document } = require('/document.js');
const { ShapeNodeDefinition } = require('/nodes.js');
const { AddChildNodesCommandBuilder } = require('/commands.js');
const {
ShapeCog, ShapeHeart, ShapeSpiral, ShapeQRCode,
QRPayloadURL, ShapeType
} = require('/shapes.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
function addShape(shape, x, y, w, h, name) {
const def = ShapeNodeDefinition.createDefault();
def.shape = shape;
def.userDescription = name;
def.setBoundingRectangle({ x, y, width: w, height: h });
const builder = AddChildNodesCommandBuilder.create();
builder.addShapeNode(def);
const cmd = builder.createCommand(true);
doc.executeCommand(cmd);
return cmd.newNodes?.[0];
}
const cog = ShapeCog.create();
cog.teeth = 12;
const cogNode = addShape(cog, 100, 100, 150, 150, 'Cog');
if (cogNode) cogNode.lineWeightPts = 1;
const heart = ShapeHeart.create();
heart.spread = 0.6;
const heartNode = addShape(heart, 300, 100, 150, 150, 'Heart');
if (heartNode) heartNode.lineWeightPts = 1;
const spiral = ShapeSpiral.create();
spiral.turns = 4;
const spiralNode = addShape(spiral, 500, 100, 150, 150, 'Spiral');
if (spiralNode) spiralNode.lineWeightPts = 1;
const url = QRPayloadURL.create();
url.url = 'https://example.com';
const qr = ShapeQRCode.create();
qr.payload = url;
const qrNode = addShape(qr, 100, 300, 150, 150, 'QR code');
if (qrNode) qrNode.lineWeightPts = 1;
for (const node of doc.layers.all) {
const si = node.shapeInterface;
if (si) {
console.log(node.descriptionInterface?.description ?? '(unnamed)', '→', String(si.type));
}
}
}
06d-shapes-and-qr.js
/*
BSD 3-Clause License
Copyright (c) 2026, Canva Pty Ltd.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Redistributions of source code must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
'use strict';
const { Dialog, DialogResult } = require('/dialog.js');
const { Document, DocumentCommand } = require('/document.js');
const { ShapeStar, ShapePolygon, ShapeRectangle, ShapeArrow, ShapeArrowEndStyle, ShapePie, ShapeQRCode, QRPayloadURL, QRPayloadVCard, QRPayloadEmail, QRPayloadSMS, QRPayloadWifi } = require('/shapes.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else { main(); }
function applyShapeToSelection(shape) {
const sel = doc.selection;
if (!sel || sel.nodes.length === 0) {
console.log('Select a shape first.');
return;
}
doc.executeCommand(DocumentCommand.createSetShape(sel, shape));
}
function demoBuiltInShapes() {
const star = ShapeStar.create();
star.innerRadius = 0.4;
star.points = 5;
star.leftCurve = 0.5;
star.rightCurve = 0.5;
applyShapeToSelection(star);
console.log('Applied Star shape');
}
function demoPolygon() {
const poly = ShapePolygon.create();
poly.sides = 6;
poly.curve = 0.2;
poly.smoothPoints = true;
applyShapeToSelection(poly);
console.log('Applied Polygon shape');
}
function demoRectangle() {
const rect = ShapeRectangle.create();
const node = doc.selection.nodes.first;
const box = node ? node.getSpreadBaseBox() : null;
const w = box ? box.width : 200;
const h = box ? box.height : 200;
rect.topLeft.setRadius(20, w, h);
rect.topRight.setRadius(20, w, h);
rect.useSingleRadius = false;
applyShapeToSelection(rect);
console.log('Applied rounded Rectangle shape');
}
function demoArrow() {
const arrow = ShapeArrow.create();
arrow.leftEnd = ShapeArrowEndStyle.None;
arrow.rightEnd = ShapeArrowEndStyle.Arrow;
arrow.thickness = 0.3;
applyShapeToSelection(arrow);
console.log('Applied Arrow shape');
}
function demoPie() {
const pie = ShapePie.create();
pie.innerRadius = 0.2;
pie.startAngle = 0;
pie.sweep = 270;
pie.isClosed = true;
applyShapeToSelection(pie);
console.log('Applied Pie shape');
}
function demoQRCode() {
const url = QRPayloadURL.create('https://affinity.serif.com');
const qrShape = ShapeQRCode.create();
qrShape.payload = url;
applyShapeToSelection(qrShape);
console.log('Applied QR code shape');
}
function demoVCard() {
const vcard = QRPayloadVCard.create();
vcard.name = 'Jane Doe';
vcard.firstName = 'Jane';
vcard.lastName = 'Doe';
vcard.email = 'jane@example.com';
vcard.phone = '+1 555 0100';
vcard.company = 'Acme Corp';
const qrShape = ShapeQRCode.create();
qrShape.payload = vcard;
applyShapeToSelection(qrShape);
console.log('Applied vCard QR code');
}
function demoWifi() {
const wifi = QRPayloadWifi.create('MyNetwork', 'secretpass', 2);
const qrShape = ShapeQRCode.create();
qrShape.payload = wifi;
applyShapeToSelection(qrShape);
console.log('Applied WiFi QR code');
}
function buildDialog() {
const dlg = Dialog.create('Shapes & QR Codes');
const col = dlg.addColumn();
const grp = col.addGroup('Built-in Shapes');
grp.addButton('Star').onClickHandler = () => { demoBuiltInShapes(); };
grp.addButton('Polygon').onClickHandler = () => { demoPolygon(); };
grp.addButton('Rounded Rect').onClickHandler = () => { demoRectangle(); };
grp.addButton('Arrow').onClickHandler = () => { demoArrow(); };
grp.addButton('Pie').onClickHandler = () => { demoPie(); };
const qrGrp = col.addGroup('QR Codes');
qrGrp.addButton('URL QR').onClickHandler = () => { demoQRCode(); };
qrGrp.addButton('vCard QR').onClickHandler = () => { demoVCard(); };
qrGrp.addButton('WiFi QR').onClickHandler = () => { demoWifi(); };
col.addGroup('').addButton('Close').onClickHandler = () => { dlg.isRunningModal = false; };
return dlg;
}
function main() {
const dlg = buildDialog();
dlg.runModal();
}