Skip to content

Bezier #50

Description

@AlkamolGradient

'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
    1. Open your document in Affinity Designer / Publisher.
    1. Select one or more curve or shape objects.
  • (If nothing is selected the script runs on ALL nodes.)
    
    1. Run via File › Scripts › Run Script…
  • or add to your Script Library for one-click access.
    
    1. 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;

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions