Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lua Plugins

NullCad can be extended with Lua 5.4 plugins. Plugins can add menu commands, interactive canvas tools, panels and dialogs, and automation hooks that react to application events. Every document mutation a script makes goes through the normal operations system, so plugin edits are undoable and appear in the audit trail like hand-drawn geometry.

Installing plugins

Plugins live in the NullCad config directory:

PlatformLocation
Linux~/.config/nullcad/plugins/
Windows%APPDATA%\nullcad\plugins\
macOS~/Library/Application Support/nullcad/plugins/

Two layouts are supported:

  • Single file — drop my_plugin.lua directly in plugins/.
  • Directory — a folder containing plugin.toml and an entry script:
# plugins/survey-helpers/plugin.toml
name = "Survey Helpers"
id = "survey-helpers"        # optional, defaults to the folder name
version = "0.1.0"
description = "Point grids, selection reports"
author = "You"
entry = "init.lua"           # optional, defaults to init.lua

Plugins load on startup. Use Plugins ▸ Reload Plugins after editing a script, and Plugins ▸ Plugin Manager… to enable/disable plugins or see load errors. Plugins ▸ Run Script… executes a one-off .lua file without installing it (useful for batch automation).

A minimal plugin

-- plugins/hello.lua
nc.register_command{
    id = "hello",
    title = "Say Hello",
    run = function(ctx)
        ctx:toast("Hello from Lua!", "success")
    end,
}

This adds Plugins ▸ hello ▸ Say Hello to the menu bar.

The nc global

Every plugin runs in its own Lua state with the nc table installed:

ItemPurpose
nc.versionNullCad version string
nc.ctxApplication context (see below)
nc.register_command{...}Add a menu command
nc.register_tool{...}Add an interactive canvas tool
nc.register_panel{...}Add a toggleable panel
nc.on(event, handler)Subscribe to an application event

All callbacks receive ctx (the same object as nc.ctx) as their first argument.

Commands

nc.register_command{
    id = "count",                 -- required, unique within the plugin
    title = "Count Geometry",     -- menu label (defaults to id)
    run = function(ctx) ... end,  -- required
}

Tools

Tools receive snapped canvas clicks in world coordinates. Return true from on_click to finish the tool (the app returns to selection mode); return false/nothing to keep receiving clicks. Tool state is just Lua upvalues.

local first = nil
nc.register_tool{
    id = "two-point",
    name = "Two Point Line",
    instructions = "Click start point, then end point",
    on_click = function(ctx, x, y)
        if not first then
            first = {x = x, y = y}
            return false
        end
        ctx:create_line{start_x = first.x, start_y = first.y, end_x = x, end_y = y}
        first = nil
        return true
    end,
}

Registered tools appear in the plugin’s menu; activating one behaves like any built-in tool (Esc cancels, snapping applies).

Panels and dialogs

UI is declarative: a list of widget tables rendered by the app. Button clicks call your on_event handler with the button id and the current values of all input widgets (keyed by widget id).

Widget types:

{type = "heading",   text = "Section"}
{type = "label",     text = "Static text"}
{type = "separator"}
{type = "text",      id = "name", label = "Name",    value = "default"}
{type = "number",    id = "dist", label = "Distance", value = 10.0}
{type = "checkbox",  id = "flag", label = "Enabled",  value = true}
{type = "select",    id = "mode", label = "Mode", options = {"A", "B"}, value = "A"}
{type = "button",    id = "go",   label = "Go"}

Panels are registered once and toggled from the plugin’s menu (or with ctx:open_panel(id) / ctx:close_panel(id)); update their contents with ctx:update_panel(id, widgets).

nc.register_panel{
    id = "quick-point",
    title = "Quick Point",
    widgets = {
        {type = "number", id = "x", label = "Easting",  value = 0.0},
        {type = "number", id = "y", label = "Northing", value = 0.0},
        {type = "button", id = "create", label = "Create Point"},
    },
    on_event = function(ctx, event, values)
        if event == "create" then
            ctx:create_point{x = values.x, y = values.y}
        end
    end,
}

Dialogs are opened from any callback with ctx:show_dialog. Return true from the handler to close the dialog; the window’s ✕ also closes it.

ctx:show_dialog{
    title = "Grid Options",
    widgets = {
        {type = "number", id = "rows",    label = "Rows",    value = 5},
        {type = "number", id = "spacing", label = "Spacing", value = 10.0},
        {type = "button", id = "ok",     label = "Create"},
        {type = "button", id = "cancel", label = "Cancel"},
    },
    on_event = function(ctx, event, values)
        if event == "ok" then make_grid(ctx, values) end
        return true  -- close on any button
    end,
}

Events (automation)

nc.on("document_saved", function(ctx, e)
    ctx:log("Saved to " .. e.path)
end)
EventPayload
document_opened{path}
document_saved{path}
document_new{}
operation_applied{description}
selection_changed{ids, count}
phase_changed{phase}

Events are delivered once per frame, after the mutation completes. Operations performed inside an operation_applied handler do not re-trigger the event (no feedback loops).

ctx reference

Coordinates are world meters (f64). Geometry and layer ids are UUID strings. Errors raise Lua errors — wrap calls in pcall if you want to recover.

Reading

MethodReturns
ctx:document_info(){project_name, file_path, geometry_count, layer_count, current_phase, active_layer_id, ...}
ctx:current_phase()"SetupSearch" | "BoundaryModel" | "Reinstatement" | "Drafting"
ctx:list_layers(){layers = {{id, name, visible, locked, color, order}, ...}}
ctx:list_geometry{layer_name=, geometry_type=, limit=}{count, items = {{id, type, name, layer_name, z, summary}, ...}} (all filters optional)
ctx:get_geometry(id)Full record incl. geometry table (e.g. {type = "Point", x, y}; Text/Rectangle include rotation_degrees, CCW from east)
ctx:selection()Array of selected geometry ids

Creating geometry

All creators accept optional layer_id/layer_name (defaults to the default layer), name, description, and (where meaningful) z. They return the new geometry’s id.

ctx:create_point{x=, y=, z=}
ctx:create_line{start_x=, start_y=, end_x=, end_y=}
ctx:create_polyline{points = {{x=, y=}, ...}}          -- ≥ 2 points
ctx:create_polygon{points = {{x=, y=}, ...}}           -- ≥ 3 points
ctx:create_circle{center_x=, center_y=, radius=}
ctx:create_arc{center_x=, center_y=, radius=, start_angle=, end_angle=}
ctx:create_rectangle{corner1_x=, corner1_y=, corner2_x=, corner2_y=}
ctx:create_text{x=, y=, content=, height=}
ctx:create_layer{name=, color="#RRGGBB"}               -- returns layer id

Modifying

MethodEffect
ctx:delete(id)Soft-delete a geometry object (undoable)
ctx:move_geometry(ids, dx, dy)Translate the given ids by a delta
ctx:rotate_geometry(ids, center_x, center_y, angle_degrees)Rotate around a center point, clockwise degrees (surveying convention, same as the Rotate tool)
ctx:set_selection(ids) / ctx:clear_selection()Change the selection

Feedback & UI

MethodEffect
ctx:toast(message, level?)Toast notification ("info", "success", "warning", "error")
ctx:log(message, level?)Write to the console (F12), attributed to the plugin
ctx:show_dialog{title=, widgets=, on_event=}Open a dialog
ctx:update_panel(id, widgets)Replace a panel’s widgets (values with matching ids survive)
ctx:open_panel(id) / ctx:close_panel(id)Show/hide a registered panel

Notes & limitations

  • Scripts run on the UI thread: a long loop blocks the interface. Keep per-callback work modest.
  • ctx is only valid while NullCad is calling into your script (load, callbacks). Don’t stash it for use from Lua coroutines/timers — there are none anyway.
  • Lua’s standard library is available (math, string, table, io, os), so scripts can read/write files for import/export automation.
  • Full examples live in docs/examples/plugins/ in the repository: survey-helpers/ (commands, tools, panels, events) and align-text.lua (rotate a text object to match a clicked line direction).