# Canvas tool plugins

Source: https://www.michaelchurley.com/blog/omadesign-0-5-8-canvas-tool-plugins  
Published: 2026-09-23  
Author: Michael C. Hurley  
Tags: omadesign, 0.5.8, tools

## The habit

The Pencil tool in Illustrator, the brush in Photoshop, and the vector brush in Affinity all share one contract. You choose the tool. You drag. You see the mark while your hand is still down. You let go and the mark belongs to the document. Escape, or another tool letter, puts the pencil down. You do not want a plugin "tool" that is secretly a dialog: click five points, press OK, and hope the path lands where the dialog guessed.

You also know the other contract, the one for pixels. A brush preset does nothing useful until a raster layer is the thing under the cursor. A filter that runs on the vector layer you forgot to leave will either error or bake the wrong target. The tool and the filter are neighbors in the plugin list. They are not the same gesture.

## The constraint

A plugin cannot draw into the canvas on its own schedule. The studio owns the pointer, the preview, and the undo step. If the plugin received every mouse move inside Lua, a slow script would lag the stroke and a crashed script would leave a private preview on screen with no object behind it. The gesture has to be recorded by the host as document points, shown as a line the host paints, and handed over once, on release, as `ctx.gesture`.

The plugin then builds native geometry and returns. That result goes through the same commit rules as any other document action: one batch, or nothing. Escape has to drop the armed tool before a release can fire. Switching to a drawing tool with a letter key has to drop it too, or you would drag a rectangle and also commit a plugin path. Pixel work stays on a raster layer because the brush and the filter write pixels, and a vector-only document has nowhere legal to put them until you add that layer.

## What landed

An action with `tool = true` is a canvas tool. In the manager its button reads **Activate tool** rather than **Run**. Studio starter's tool is **Ribbon path**, command id `ribbon`, category Tools. Stroke width defaults to 14, range 1 to 100. Color defaults to `#A6E3A1`.

Press **Activate tool**. The Plugins menu grows **Exit plugin tool  Esc** while it is armed. The cursor on the canvas is a crosshair. Press the pointer and drag. The host records points in document coordinates, including whether Alt, Shift, or Ctrl were held. It will keep up to 8192 points. While the button is down it paints a 1.5 pixel line in the accent color. That line is a preview. It is not a shape, it has no layer row, and it is not in the `.oma`.

Release the button. The host starts the plugin with `ctx.gesture.points`. Ribbon path requires at least two points. It calls `oma.add_shape` with `kind = "path"`, those points, fill `none`, the stroke color and width you set, and the name Ribbon. The commit is one Undo step. The path is editable with the Node tool, `A`, like any pencil stroke.

Escape does two pieces of work. It clears the armed plugin tool and sets the status to **Plugin tool stopped**. It also follows the studio's normal Escape rules if a different operation was in progress. Choosing another studio tool, anything other than the Move tool, also clears the plugin tool so the letter keys keep their meaning. Space-pan is left to the hand tool path. The plugin does not receive the gesture while you are panning.

A click or a drag that never builds two points fails the plugin's assert. You get the error. You do not get a stray one-point object.

Pixel filters and brush presets are the other half of the tweet, and they are not this gesture. **Midnight duotone** needs `ctx.active_layer` on a raster layer. The starter asserts **Select a raster layer first**. **Soft ink brush** and **Dry marker brush** call `oma.brush` and the studio moves you to the Pixel persona with the Brush tool selected. Painting still happens on a raster layer. If the document is vectors only, add a pixel layer from the Layers studio before you expect ink to stick. The manual's line is the short version: choose a Raster document and a raster layer before pixel filters or brush presets.

## In the hand

Open a vector document. **Plugins → Manage plugins**. Select **Tools · Ribbon path**. Set Stroke width if 14 is wrong. Press **Activate tool**. The manager can stay open or you can go back to the canvas. The status line already told you to choose parameters, then Run or Activate tool, when you opened the action.

Drag a curve. Watch the line. It tracks the pointer and it does not create a layer. Release. A path named Ribbon is selected in the stack, stroked, no fill. Press `Ctrl+Z`. The path goes away in one step. Press `A` and the nodes are there if you redo.

Arm the tool again. Drag a stroke you do not like and press Escape before you release. Status says **Plugin tool stopped**. Release does nothing useful because the tool is gone. Arm it again and press `P` for the Pen. The plugin tool drops because you left the Move tool. The Pen works as the Pen.

For pixels, make or open a Raster document. Select the raster layer in the layer list. Run **Filters · Midnight duotone**. The active layer's pixels change. Alpha stays. Then run **Brushes · Dry marker brush**. The persona switches to Pixel. Paint. `[` and `]` still change brush size after the preset loads. The preset did not lock the tool.

If you run the duotone with no raster layer active, the plugin stops on **Select a raster layer first** and the document does not change.

## The edge

The preview line is not artwork. Until release, the `.oma` is unchanged, and Escape throws the gesture away. A finished stroke is one Undo, and a failed assert or a cancel applies nothing. The tool also refuses to stay armed once you pick a different studio tool, so a plugin cannot hijack `R` or `P` or `B`.

Filters and brushes refuse the vector-only case. They need a raster layer. The ribbon does not write pixels, and the duotone does not read your drag.

Press **Activate tool**, drag the stroke, and release. Escape if the line is wrong.


## The thread

Part 124 of 144 in the Omadesign 0.5.8 feature thread.

[Previous](/blog/omadesign-0-5-8-plugin-categories) · [Next](/blog/omadesign-0-5-8-opt-in-behaviors)
