'use strict';
/**
- ============================================================
- BEZIER VISUALISER — Gridit equivalent for Affinity
- ============================================================
- Author : Built for Pictodient (@pictodient)
- Version : 2.1 — Verified working, no dialog required
- Draws the full bezier skeleton of selected curve / shape
- nodes as real, editable vector objects:
- ■ Anchor points (on-curve nodes) — filled squares
- ○ Control handles (off-curve points) — hollow circles
- — Tangent lines (anchor ↔ handle) — thin lines
- HOW TO USE
-
- Open your document in Affinity Designer / Publisher.
-
- Select one or more curve or shape objects.
-
(If nothing is selected the script runs on ALL nodes.)
-
- Run via File › Scripts › Run Script…
-
or add to your Script Library for one-click access.
-
- The skeleton is drawn on top of your artwork.
-
Press Cmd/Ctrl+Z once to remove the entire overlay.
- HOW TO CUSTOMISE
- Edit the CONFIG block below — no dialog needed.
- autoScale = true → sizes scale with your document width
- autoScale = false → use your own fixed pixel values
- ============================================================
*/
// ─── CONFIG ── edit these values to customise ───────────────
const CONFIG = {
autoScale: true, // true = auto-scale sizes to doc width
// Fixed sizes in document units (used when autoScale=false)
anchorSize: 6, // half-width of anchor square (px)
handleSize: 4, // radius of handle circle (px)
lineWeight: 1, // stroke weight (px)
// Colours as { r, g, b } (values 0–255)
anchorColour: { r: 17, g: 85, b: 255 }, // blue
handleColour: { r: 255, g: 102, b: 0 }, // orange
tangentColour: { r: 140, g: 140, b: 140 }, // grey
};
// ─────────────────────────────────────────────────────────────
// Internal — no need to edit below this line
// ─────────────────────────────────────────────────────────────
const { Document } = require('/document');
const { AddChildNodesCommandBuilder,
CompoundCommandBuilder } = require('/commands');
const { PolyCurveNodeDefinition } = require('/nodes');
const { CurveBuilder, PolyCurve,
Curve, Rectangle } = require('/geometry');
const { FillDescriptor } = require('/fills');
const { LineStyleDescriptor, LineStyle } = require('/linestyle');
const { Colour, RGBA8 } = require('/colours');
const { BlendMode } = require('affinity:common');
// ── Helpers ──────────────────────────────────────────────────
function rgba(r, g, b, a = 255) {
return Colour.createRGBA8(new RGBA8(r, g, b, a));
}
function solidFill(col) {
return FillDescriptor.createSolid(rgba(col.r, col.g, col.b), BlendMode.Normal);
}
function makeLSD(weight) {
return LineStyleDescriptor
.createDefault()
.cloneWithNewLineStyle(LineStyle.createDefaultWithWeight(weight));
}
function makeSquarePC(cx, cy, half) {
const pc = PolyCurve.create();
pc.addCurve(Curve.createRectangle(new Rectangle(cx - half, cy - half, half * 2, half * 2)));
return pc;
}
function makeCirclePC(cx, cy, r) {
const pc = PolyCurve.create();
pc.addCurve(Curve.createEllipse(new Rectangle(cx - r, cy - r, r * 2, r * 2)));
return pc;
}
function makeLinePC(x1, y1, x2, y2) {
const cb = CurveBuilder.create();
cb.beginXY(x1, y1).lineToXY(x2, y2);
const pc = PolyCurve.create();
pc.addCurve(cb.createCurve());
return pc;
}
function pushNode(compound, polyCurve, brushFill, penFill, lsd) {
const def = PolyCurveNodeDefinition.create(
polyCurve, brushFill, lsd, penFill, FillDescriptor.createNone()
);
const b = AddChildNodesCommandBuilder.create();
b.addPolyCurveNode(def);
compound.addCommand(b.createCommand(false));
}
function canVisualise(n) {
const t = n.constructor.name;
return t === 'PolyCurveNode' || t === 'ShapeNode';
}
// ── Per-node skeleton builder ─────────────────────────────────
function visualiseNode(node, compound, opts) {
const noFill = FillDescriptor.createNone();
const anchorFill = solidFill(opts.anchorColour);
const anchorPen = solidFill(opts.anchorColour);
const handlePen = solidFill(opts.handleColour);
const tangentPen = solidFill(opts.tangentColour);
const lsd = makeLSD(opts.lineWeight);
let ci; try { ci = node.curvesInterface; } catch (_) { return 0; }
if (!ci) return 0;
let pc; try { pc = ci.polyCurve; } catch (_) { return 0; }
if (!pc || pc.curveCount === 0) return 0;
let drawn = 0;
for (let i = 0; i < pc.curveCount; i++) {
const curve = pc.at(i);
const seen = new Set();
const markAnchor = (pt) => {
const key = `${Math.round(pt.x * 10)},${Math.round(pt.y * 10)}`;
if (seen.has(key)) return;
seen.add(key);
pushNode(compound, makeSquarePC(pt.x, pt.y, opts.anchorSize), anchorFill, anchorPen, lsd);
drawn++;
};
for (const bez of curve.beziers) {
const { start, c1, c2, end } = bez;
markAnchor(start);
// Outgoing handle c1 — only when offset from anchor
if (Math.hypot(c1.x - start.x, c1.y - start.y) > 0.5) {
pushNode(compound, makeLinePC(start.x, start.y, c1.x, c1.y), noFill, tangentPen, lsd);
pushNode(compound, makeCirclePC(c1.x, c1.y, opts.handleSize), noFill, handlePen, lsd);
drawn += 2;
}
// Incoming handle c2 — only when offset from end anchor
if (Math.hypot(c2.x - end.x, c2.y - end.y) > 0.5) {
pushNode(compound, makeLinePC(end.x, end.y, c2.x, c2.y), noFill, tangentPen, lsd);
pushNode(compound, makeCirclePC(c2.x, c2.y, opts.handleSize), noFill, handlePen, lsd);
drawn += 2;
}
}
// Final closing anchor
try { markAnchor(curve.getPoint(curve.lastOnCurvePointIndex)); } catch (_) {}
}
return drawn;
}
// ── Entry point ───────────────────────────────────────────────
function main() {
const doc = Document.current;
if (!doc) { alert('Bezier Visualiser: No document is open.'); return; }
// Resolve sizes — auto-scaled or fixed
const docW = doc.widthPixels || 1000;
const opts = CONFIG.autoScale
? {
anchorSize: Math.max(3, Math.round(docW * 0.004)),
handleSize: Math.max(2, Math.round(docW * 0.003)),
lineWeight: Math.max(1, Math.round(docW * 0.001)),
anchorColour: CONFIG.anchorColour,
handleColour: CONFIG.handleColour,
tangentColour: CONFIG.tangentColour,
}
: {
anchorSize: CONFIG.anchorSize,
handleSize: CONFIG.handleSize,
lineWeight: CONFIG.lineWeight,
anchorColour: CONFIG.anchorColour,
handleColour: CONFIG.handleColour,
tangentColour: CONFIG.tangentColour,
};
// Collect targets: selection first, then all nodes as fallback
const targets = [];
const sel = doc.selection.nodes;
if (sel && sel.length > 0) {
for (const n of sel) { if (canVisualise(n)) targets.push(n); }
}
if (targets.length === 0) {
for (const n of doc.layers) { if (canVisualise(n)) targets.push(n); }
}
if (targets.length === 0) {
alert('Bezier Visualiser: No curve or shape objects found.\nSelect at least one curve or shape node and try again.');
return;
}
// Build all draw commands as one single undoable action
const compound = CompoundCommandBuilder.create();
let total = 0;
for (const n of targets) total += visualiseNode(n, compound, opts);
if (total === 0) {
alert('Bezier Visualiser: No bezier data could be read from the selected objects.');
return;
}
doc.executeCommand(compound.createCommand());
console.log(`Bezier Visualiser: drew ${total} elements across ${targets.length} node(s). Undo once to remove.`);
}
module.exports.main = main;
'use strict';
/**
*/
// ─── CONFIG ── edit these values to customise ───────────────
const CONFIG = {
autoScale: true, // true = auto-scale sizes to doc width
};
// ─────────────────────────────────────────────────────────────
// Internal — no need to edit below this line
// ─────────────────────────────────────────────────────────────
const { Document } = require('/document');
const { AddChildNodesCommandBuilder,
CompoundCommandBuilder } = require('/commands');
const { PolyCurveNodeDefinition } = require('/nodes');
const { CurveBuilder, PolyCurve,
Curve, Rectangle } = require('/geometry');
const { FillDescriptor } = require('/fills');
const { LineStyleDescriptor, LineStyle } = require('/linestyle');
const { Colour, RGBA8 } = require('/colours');
const { BlendMode } = require('affinity:common');
// ── Helpers ──────────────────────────────────────────────────
function rgba(r, g, b, a = 255) {
return Colour.createRGBA8(new RGBA8(r, g, b, a));
}
function solidFill(col) {
return FillDescriptor.createSolid(rgba(col.r, col.g, col.b), BlendMode.Normal);
}
function makeLSD(weight) {
return LineStyleDescriptor
.createDefault()
.cloneWithNewLineStyle(LineStyle.createDefaultWithWeight(weight));
}
function makeSquarePC(cx, cy, half) {
const pc = PolyCurve.create();
pc.addCurve(Curve.createRectangle(new Rectangle(cx - half, cy - half, half * 2, half * 2)));
return pc;
}
function makeCirclePC(cx, cy, r) {
const pc = PolyCurve.create();
pc.addCurve(Curve.createEllipse(new Rectangle(cx - r, cy - r, r * 2, r * 2)));
return pc;
}
function makeLinePC(x1, y1, x2, y2) {
const cb = CurveBuilder.create();
cb.beginXY(x1, y1).lineToXY(x2, y2);
const pc = PolyCurve.create();
pc.addCurve(cb.createCurve());
return pc;
}
function pushNode(compound, polyCurve, brushFill, penFill, lsd) {
const def = PolyCurveNodeDefinition.create(
polyCurve, brushFill, lsd, penFill, FillDescriptor.createNone()
);
const b = AddChildNodesCommandBuilder.create();
b.addPolyCurveNode(def);
compound.addCommand(b.createCommand(false));
}
function canVisualise(n) {
const t = n.constructor.name;
return t === 'PolyCurveNode' || t === 'ShapeNode';
}
// ── Per-node skeleton builder ─────────────────────────────────
function visualiseNode(node, compound, opts) {
const noFill = FillDescriptor.createNone();
const anchorFill = solidFill(opts.anchorColour);
const anchorPen = solidFill(opts.anchorColour);
const handlePen = solidFill(opts.handleColour);
const tangentPen = solidFill(opts.tangentColour);
const lsd = makeLSD(opts.lineWeight);
}
// ── Entry point ───────────────────────────────────────────────
function main() {
const doc = Document.current;
if (!doc) { alert('Bezier Visualiser: No document is open.'); return; }
}
module.exports.main = main;