Book › Developers › Writing a symmetry tool
Writing a symmetry tool
A symmetry mode is a plugin: one class extending SymmetryTool (with an icon and a create()), plus a one-line row in symmetry.json - the same shape as brushes.
What a symmetry tool is
Symmetry is a global tool that repeats any brush. A tool's whole
job is to turn one stroke into a list of affine copies
(Transform[]). A proxy around the drawing surface then replays
every mark and every deposited point at each transform, so the
connecting web threads across the pattern, not
just one copy. Tile, Radial, Mirror, Concentric and Spiral all live in
src/symmetry/tools/ - one file each.
A minimal tool
Extend SymmetryTool, implement transforms(), and
(optionally) declare panel controls and guide lines. Here is an "Echo" that
stamps fading offset copies of the stroke:
// src/symmetry/tools/echo.ts
import { SymmetryTool, type SymSetting, type ToolContext } from "../tool";
import type { Transform } from "../transforms";
export const icon = "…svg markup…";
class EchoTool extends SymmetryTool {
// Copies are a fixed offset from the stroke, so no centre is needed.
// (Set `usesCentre = true` to get the shared Centre sliders + ctx.cx/cy.)
count = 4;
offset = 24;
// Each Transform is an affine map x' = a*x + c*y + e, y' = b*x + d*y + f,
// plus aMul (this copy's opacity). Include an identity copy to draw the
// original stroke - here k = 0 is identity.
transforms(_ctx: ToolContext): Transform[] {
const out: Transform[] = [];
for (let k = 0; k < this.count; k++)
out.push({ a: 1, b: 0, c: 0, d: 1, e: k * this.offset, f: 0, aMul: 1 - k / this.count });
return out;
}
settings(): SymSetting[] {
return [
{ kind: "slider", key: "count", label: "Copies", min: 2, max: 10, value: this.count, onChange: (v) => (this.count = v) },
{ kind: "slider", key: "offset", label: "Offset", min: 4, max: 120, value: this.offset, onChange: (v) => (this.offset = v) },
];
}
}
export const create = () => new EchoTool();
Register it
Export the glyph + a create() from the file, then add one row
to src/symmetry/symmetry.json. The panel picker and the navbar
Symmetry combo are generated from there, in file order - nothing else to
touch.
// src/symmetry/symmetry.json - add one row (order = mode-picker order)
{ "name": "echo", "file": "echo.ts", "label": "Echo", "info": "Fading offset copies." }
The SymmetryTool surface
transforms(ctx): Transform[]- The only required method. Frozen once at pointer-down for the whole stroke.
ctxcarries the stroke anchor (startX/startY), the canvassize, the resolved centre (cx/cy) and theguidestyle. settings(): SymSetting[]- Declarative controls -
slider,toggleorsegment. The framework renders each row and persists it underapp.symmetry.<name>.<key>. Setdisabledto grey one out, orpersist:falsefor a derived control (Mirror's V/H buttons drive the Angle slider). drawGuides(r, ctx)- Optional on-canvas guide geometry. The framework clears the overlay first and, for centred tools, draws the centre crosshair after.
usesCentre- Set
trueto get the shared movable Centre sliders + Recentre button, with the pivot resolved intoctx.cx/cy. Tile leaves itfalseand pivots on the stroke start. mirrorsPoints(): boolean- Whether deposited points are mirrored into the searchable cloud (default true). Return false when a mode floods so many copies it would evict the cloud - Tile's "Fill canvas" does.
What you get for free
- Discovery:
symmetry.json+ a Vite glob overtools/*.tsbuild the registry (seesrc/symmetry/registry.ts). - The panel:
settings()is rendered generically, plus the shared Centre and guide-style controls. - Persistence: each setting saves + restores under its own key namespace.
- The proxy: marks and points are replayed at every
Transform(src/symmetry/proxy.ts), so the web spans the symmetry. - Affine builders in
transforms.ts-translate,rotateReflect,reflectAcrossLine,scaleRotateAbout(plusapplyPoint/applyAngle/IDENTITY). That file is a small shared matrix library only; each mode's loops, caps and params live in its own tool, so adding a mode touches just the new file + the JSON row.
transforms() bounded - it runs on every pointer move and each
copy redraws the stroke and deposits points (cap big counts, as Spiral
does). For the user-facing guide to each mode, see
Symmetry.