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

NullCad

NullCad is a 2D CAD application purpose-built for cadastral surveying, built with Rust, egui, and a custom wgpu renderer. It is designed to provide a modern, efficient, and precise alternative to the big name CAD programs.

Most CAD software is designed for engineers with surveying being an afterthought or functionality added through seperate plugins that are never quite able to overcome the inherint limitations of the native DXF/DWG file format.

This book covers how to use NullCad day to day — navigation, drawing and modify tools, snapping, layers, the survey workflow, drafting and PDF output, import/export formats, and the plugin/automation systems — as well as the file format and internal architecture for anyone extending the application.

Where to start

Highlights

  • Phase-based survey workflow with independent camera state and status tracking per phase.
  • Full snap tracking for easy and efficient drawing and editing of survey geometry.
  • Vector PDF export of drafting sheets, with viewports, sheet templates, and title-block substitution.
  • Pure-Rust import/export for Shapefile, GeoJSON, DXF, and CSV, with optional GDAL support for KML, GeoPackage, MapInfo TAB, and FileGDB.
  • Lua plugin system and an in-process MCP server for scripting and AI agent integration.
  • ZIP-based .nullcad archive format with full operation history, undo audit trail, and embedded resources.

License

Copyright 2026. All rights reserved.

Installation

Pre-built installers are published on the GitHub Releases page for each tagged version:

PlatformFormats
Linux.deb, .pacman
Windows.nsis installer

Download the installer for your platform from the latest release and run it.

If you’re on a platform without a pre-built installer, or you want the latest unreleased changes, see Building from Source.

Requirements

  • A GPU with wgpu/WebGPU support (essentially any GPU from the last decade, on Vulkan, Metal, DirectX 12, or OpenGL).
  • GDAL is not required for the default build — it’s only needed if you build with the optional gdal feature for KML/GeoPackage/MapInfo/FileGDB support.

Interface Overview

NullCad’s main window is organized around a central canvas viewport, surrounded by panels for layers, properties, tools, and phase status.

  • Phase tabs — switch between the four survey phases (Setup/Search, Boundary Model, Reinstatement, Drafting). Each phase has independent camera state and status tracking; see Survey Phases.
  • Toolbar — grouped by category: Select, Draw, Modify, Survey, Measure, Surface & Drafting. See Drawing Tools and Modify Tools.
  • Layer panel — create, rename, delete, and organize layers with groups; per-layer color, visibility, lock state, and line type. See Layers.
  • Properties panel — inspect and edit the properties of the current selection.
  • Canvas viewport — the main drawing surface, rendered with a custom wgpu pipeline for precision at large coordinates (see Precision Handling).
  • Status bar — coordinates under the cursor, active tool, snap state.
  • Console log (F12) — a searchable, color-coded, persistent log of all application messages.

Theme

NullCad uses the Catppuccin color scheme.

Distraction-free mode

Press F11 to hide the side panels and show a floating toolbox — useful when you want the canvas to take up the full window.

Settings

Application settings (point size, selection threshold, colors, line weight, label display, camera sensitivity, autosave interval, and more) are available from the Settings menu, organized into categories such as Settings ▸ Interaction and Settings ▸ Labels.

Recent files

The File menu keeps a list of recently opened projects for quick access.

View bookmarks

Save up to 9 camera positions and recall them instantly:

ActionInput
Recall bookmark1–9
Set bookmarkCtrl+1–9

Navigation & Controls

ActionInput
PanMiddle mouse drag
ZoomScroll wheel (zooms toward cursor)
Reset cameraReset Camera button
Distraction-free modeF11
View bookmarks1–9 to recall, Ctrl+1–9 to set

Panning and zooming are per-phase — each of the four survey phases keeps its own independent camera position, so switching phases doesn’t lose your place.

Editing

ActionInput
UndoCtrl+Z
RedoCtrl+Y or Ctrl+Shift+Z
Delete selectedDelete
ExplodeB
Transform gizmo (move/rotate selection)W
Toggle consoleF12

Undo/redo operates on the full operation history — every mutation is recorded and reversible. See Operation System.

A full, searchable list of shortcuts is available from the in-app keyboard shortcuts help dialog, and summarized in Keyboard Shortcuts & Controls.

Selection

Basic selection

ActionInput
SelectClick on geometry
Multi-selectCtrl+Click
Rectangle selectClick and drag on empty canvas
Select allCtrl+A

Selection uses an R-tree spatial index for efficient hit testing, so selecting and range-querying stays fast even on large datasets.

Hover highlighting

Geometry under the cursor is highlighted before you click, giving visual feedback about what a click will select.

Selection sets

Save the current selection as a named set and restore it later — useful for repeatedly working with the same group of geometry (e.g. all boundary points, or all geometry on a particular layer).

Selection history

Navigate back and forward through previous selections, similar to browser history.

Select Similar

Filter and select geometry by type, layer, code, name, position, or length — useful for bulk operations like “select all points with code BNDRY” or “all lines on this layer.”

Context menus

Right-click on geometry or empty canvas for context-sensitive actions.

Drawing Tools

Geometry primitives

NullCad supports the following geometry primitives, all stored with full f64 precision:

  • Point
  • Line
  • Polyline
  • Polygon
  • Rectangle
  • Circle
  • Arc
  • Text

Draw toolbar

The Draw tool category provides: Point, Line, Polyline, Leader, Polygon, Text, Callout, Circle, Arc, and Rectangle.

Drawing tools respect the active snap configuration — clicks snap to endpoints, midpoints, intersections, grid points, and other configured snap targets by default.

Vertex editing

Polylines and polygons support direct vertex editing: add, remove, and move individual vertices after the geometry is created.

Intersection calculations

NullCad can compute intersection points between geometry segments — useful for finding where two lines or boundaries cross without manually calculating coordinates.

Line types

Line styles (dashed, dotted, center, etc.) are configurable per layer or per individual geometry object. Custom linetype patterns can be imported from AutoCAD .lin files.

Every draw operation is undoable

Like all mutations in NullCad, drawing goes through the operation system — every draw action is recorded, undoable, and tagged with the active survey phase.

Modify Tools

Basic modify tools

  • Move — translate selected geometry.
  • Rotate — rotate selected geometry around a point.
  • Scale — scale selected geometry.
  • Trim/Extend — trim or extend lines/polylines to a boundary.
  • Offset — create a parallel copy of a line or polyline at a fixed distance.

Transform gizmo

Press W to activate the transform gizmo on the current selection: a drag-based move/rotate handle with snapping, angle snap, and a relocatable pivot point. This is the fastest way to reposition or reorient a group of geometry interactively.

Explode

Press B to break compound geometry into simpler components — polygons become polylines, polylines become individual lines.

Vertex editing

Add, remove, and modify individual vertices on polylines and polygons directly, without recreating the geometry.

Undo/redo

Every modify operation is recorded in the operation history and fully reversible with Ctrl+Z / Ctrl+Y. See Operation System.

Snapping

NullCad supports several snap modes, which apply while using drawing and modify tools:

  • Endpoint — snap to the start/end of a line or polyline segment.
  • Midpoint — snap to the midpoint of a segment.
  • Nearest — snap to the closest point on a geometry.
  • Grid — snap to a configurable grid spacing.
  • Intersection — snap to the intersection of two line segments.
  • Perpendicular — snap from a base point perpendicular to a line.
  • Angle constraint — constrain the next point to specific angle increments from the previous point.

Active snap modes

Rather than always snapping to whatever is closest, you can manually select which intersection to use when multiple candidates are nearby, or average multiple points together to derive a snap location.

Configuring snapping

Snap settings (which modes are active, grid spacing, tolerance) are available from the snap configuration panel and persist with the document state.

Layers

Layer management

Create, rename, delete, and organize layers from the layer panel. Layers can be grouped for easier organization on larger projects.

Layer properties

Each layer has:

  • Color
  • Visibility
  • Lock state — locked layers can’t be edited or selected
  • Line type
  • Label visibility — see Annotations & Labels

Phase-based filtering

Geometry is tagged with the survey phase it was created in (see Survey Phases), and the canvas filters what’s shown accordingly — you generally only see geometry relevant to the current phase, plus anything explicitly composed into a Drafting viewport.

Codelists

Codelists map survey codes to layers and layer properties (color, line type, line weight). During import, matching a geometry’s code against a codelist auto-creates and assigns the appropriate layer — useful for bringing in field data that uses a consistent coding scheme (e.g. from a data collector).

Annotations & Labels

Text annotations

Add positioned text labels with configurable style and anchoring.

Callout annotations

Text with a leader line and configurable arrowhead — useful for pointing at a specific feature from a label placed nearby.

Point labels

Point geometry can be automatically labeled based on its metadata (code, description, name), following cartographic best practices to avoid label overlap.

Label content

  • Default format: {code} {description}
  • Supported placeholders: {code}, {description}, {name}
  • Empty labels (no content) are automatically hidden

Label placement

  • Labels are positioned at the point location, with collision detection to prevent overlaps.
  • Priority system: labels on the active layer are placed first.
  • After priority sorting, remaining ties are broken top-to-bottom.

Zoom behavior

Labels maintain a constant screen size as you zoom, rather than scaling with world geometry:

  • Target screen height: 12.0 px (configurable 8.0–24.0 px).
  • World size is clamped between a minimum (default 0.5) and maximum (default 50.0) so labels stop shrinking/growing past those bounds at extreme zoom.

Performance

  • Maximum label count (default 1000).
  • Only labels within the visible camera bounds are generated (frustum culling).
  • A minimum screen-height threshold prevents rendering labels too small to read.

Visibility controls

  • Global toggle: Settings ▸ Labels ▸ Enable Point Labels.
  • Per-layer toggle: the 🏷 button in the layer panel, next to the lock button. Labels also respect the layer’s own visibility.

Settings reference (Settings ▸ Labels)

SettingRangePurpose
Enable Point Labelson/offMaster toggle
Max Labels100–5000Cap on displayed labels
Target Screen Height8.0–24.0 pxDesired constant on-screen label height
Min World Size0.1–10.0Smallest label size when zoomed in
Max World Size10.0–200.0Largest label size when zoomed out
Min Screen Height4.0–20.0 pxCulling threshold for tiny labels
Label Formattemplate stringe.g. {code}: {description}

Usage

  1. Create points with a code and description (e.g. via CSV import or manually).
  2. Enable labels: Settings ▸ Labels ▸ Enable Point Labels.
  3. Toggle per-layer visibility with the 🏷 button in the layer panel.
  4. Customize the label format string, e.g. {code}: {description}.

Survey Phases

NullCad organizes work into four phases, each with independent camera state and status tracking. Geometry and operations are tagged with the phase they’re created in, so switching phases filters what’s shown and keeps work-in-progress in one phase from cluttering another.

PhasePurpose
Setup/SearchA virtual desk for laying out plans, research, and annotations before modeling begins.
Boundary ModelBuilding the surveyable model from historical plans. This is the default phase.
ReinstatementComparing field measurements against the boundary model and adjusting.
DraftingFinalizing the plan for submission — page proxies, viewports, PDF export.

Switching phases

Use the phase tabs at the top of the window. Each phase remembers its own camera pan/zoom position independently, so returning to a phase puts you back where you left off.

Why phase-based?

Cadastral survey work naturally moves through stages — research, modeling, field verification, and final drafting — each with different working material and different geometry. Tagging geometry by phase lets each stage stay visually uncluttered by the others, while still keeping everything in one document with a single operation history and undo/redo stack.

Phase status tracking

Each phase tracks its own completion status, useful for keeping a survey project’s overall progress visible at a glance.

  • Traverse Tool — used mainly in Boundary Model, to build the model from bearings and distances.
  • Comparison Tool — used in Reinstatement, to compare field measurements against the model.
  • Page Proxies & Viewports — used in Drafting, to lay out final sheets.

Traverse Tool

The Traverse tool builds connected survey traverses by entering bearings and distances, rather than clicking points on the canvas — the standard way boundary geometry is specified on a plan of survey.

Basic use

  1. Select the Traverse tool from the Survey tool category.
  2. Enter a starting point (or snap to an existing one).
  3. Enter a bearing and distance for each subsequent leg.
  4. Each leg is added to a connected polyline as you go.

Bearing format

Bearings are entered and displayed in degrees-minutes-seconds (DMS), rounded to the nearest second — the surveying standard — not decimal degrees.

The default input is HP notation (DDD.MMSS), where the digits after the decimal point encode minutes and seconds rather than a decimal fraction of a degree:

InputParsed as
123.2312123°23’12“
45.3045°30’00“
9090°00’00“
45 30 1545°30’15“ (space-separated DMS also accepted)

To enter true decimal degrees instead, prefix the value with d (e.g. d45.5 → 45.5° = 45°30’00“). Append * to reverse a bearing by 180°.

Scale factor support

The Traverse tool supports a Combined Scale Factor (CSF) for grid-to-ground distance conversion, so entered distances can be treated as either ground distances or grid distances depending on how your survey data was recorded. See Combined Scale Factor for the formula and when to apply it.

Where this fits in the workflow

The Traverse tool is most commonly used in the Boundary Model phase, building the surveyable model from bearings and distances taken off historical plans. See Survey Phases.

Comparison Tool

The Comparison tool compares field measurements against the boundary model, using bearing/distance input — the core of the Reinstatement phase (see Survey Phases).

Purpose

After building a boundary model from historical plans, field survey work verifies that model against measurements taken on the ground. The Comparison tool records those field bearing/distance observations against model points and stores the comparison results with the document.

Comparison storage

Comparisons are stored in the document (document/comparison.rs), so comparison history is preserved with the project and available for review or reporting alongside the rest of the survey data.

Bearing format

As with the Traverse tool, bearings are entered in DMS/HP notation (DDD.MMSS), not decimal degrees. The comparison table displays the survey bearing, measured bearing, and the delta between them each rounded to the nearest arcsecond (e.g. 045° 30' 00") rather than showing fractional seconds, keeping the table readable.

Combined Scale Factor

The Combined Scale Factor (CSF) converts between grid coordinates (on the map projection) and ground/plane coordinates (real-world distances). NullCad uses the following formula for MGA (Map Grid of Australia) / UTM projections.

Formula

CSF = PSF × HSF

Where:

  • CSF = Combined Scale Factor
  • PSF = Point Scale Factor (accounts for map projection distortion)
  • HSF = Height Scale Factor (accounts for elevation above the ellipsoid)

Point Scale Factor (PSF)

PSF = 0.9996 + 1.23 × (E - 500000)² × 10⁻¹⁴

Where:

  • E = Easting coordinate (metres)
  • 500000 = False easting (central meridian offset for UTM/MGA zones)
  • 0.9996 = Central meridian scale factor (k₀) for UTM

PSF equals exactly 0.9996 at the central meridian (E = 500,000m) and increases as you move east or west.

Height Scale Factor (HSF)

HSF = 1 - (H × 0.1571 × 10⁻⁶)

Where:

  • H = Ellipsoidal height (metres)
  • 0.1571 × 10⁻⁶ ≈ 1 / 6,371,000 (approximation of 1/Earth radius)

HSF reduces distances as elevation increases, since higher points are further from Earth’s centre.

Implementation (pseudocode)

def get_csf(easting, northing, height):
    # Point Scale Factor
    psf = 0.9996 + 1.23 * (easting - 500000)**2 * 1e-14

    # Height Scale Factor
    hsf = 1 - (height * 0.1571e-6)

    # Combined Scale Factor
    return psf * hsf

Usage

DirectionOperation
Grid → GroundDivide by CSF (or multiply by 1/CSF)
Ground → GridMultiply by CSF

Worked example

For a point at E 456,789.000, N 6,123,456.000, H 150.000:

PSF = 0.9996 + 1.23 × (456789 - 500000)² × 10⁻¹⁴
    = 0.9996 + 1.23 × 1,867,422,121 × 10⁻¹⁴
    = 0.9996 + 0.00002297
    = 0.99982297

HSF = 1 - (150 × 0.1571 × 10⁻⁶)
    = 1 - 0.00002357
    = 0.99997643

CSF = 0.99982297 × 0.99997643
    = 0.99979941

A 100m ground distance would be 99.9799m on the grid.

Where to configure it

The active CSF is set in the survey settings and applied by the Traverse tool when converting between grid and ground distances.

Caveat

This formula is a simplified approximation suitable for most surveying work within a single UTM/MGA zone. For high-precision geodetic work spanning zone boundaries or requiring rigorous error bounds, use the full Transverse Mercator equations instead.

TIN Surfaces

NullCad can build Triangulated Irregular Network (TIN) surfaces from selected points, using a Constrained Delaunay Triangulation.

Creating a surface

  1. Select the points you want to triangulate.
  2. Activate the TIN Surface tool (Surface & Drafting category).
  3. Optionally add hard or soft breaklines to constrain the triangulation along known features (e.g. a ridge line or a drainage line).

Non-destructive

Building a TIN surface never modifies the source points — the surface is a separate geometry object (TinSurface) that references the input data, so you can regenerate or delete the surface without losing the underlying point data.

Breaklines

  • Hard breaklines force triangle edges to follow the breakline exactly — used for discontinuities like retaining walls or building edges.
  • Soft breaklines influence triangulation without forcing a hard edge — used for smoother features like ridge or valley lines where a small amount of triangle flexibility is acceptable.

Page Proxies & Viewports

The Drafting phase (see Survey Phases) is where a survey project becomes a finished plan for lodgement. Two geometry types drive this: page proxies and viewports.

Page proxies

A page proxy lays out a drafting sheet at a chosen draw scale, on one of the standard paper sizes:

  • ISO A-series (A0–A4)
  • US (Letter, Legal, Tabloid, …)
  • ANSI (A–E)
  • ARCH (A–E)

Each page proxy becomes one page in the exported PDF (see PDF Export). A page can be assigned a sheet template (a title block) — see below.

Sheet templates

A sheet template supplies the title-block linework and text placed on top of a page’s content, with substitutable fields like <PROJECT>, sheet number, and sheet name.

Sheet templates can be imported directly from a DXF file’s paper-space layout: NullCad recovers the layout’s true physical paper size from the DXF’s embedded plot-settings (not just its arbitrary limits/extents), and rescales non-millimeter content to fit. INSERT block references (common for real title blocks) are expanded recursively.

Assigning a template to a sheet resizes the page to the smallest standard paper size that fully contains the template, so the template’s linework — laid out in its own physical millimeter coordinates — always ends up inside the visible page rather than centered against a mismatched size.

The Sheet Manager (accessible from the Drafting phase) lists all sheets in the document, lets you assign templates, fill in title-block field values, and reorder/renumber sheets.

Viewports

A viewport is a window placed on a Drafting-phase page that displays content from another phase (most commonly Boundary Model), with its own independent pan, zoom, and rotation — similar to a viewport in traditional CAD software. This is how survey/model geometry actually ends up on a printed sheet: you draft the page and title block in the Drafting phase, then open one or more viewports onto the phase that holds the real geometry.

Draw scale

Each page proxy has its own draw scale, so different sheets in the same document can be laid out at different scales (e.g. an overview sheet at 1:1000 and a detail sheet at 1:200). Linetype patterns (dashes, dots) on the Drafting-phase canvas follow whichever page’s draw scale the camera is currently nearest to, so on-screen dash spacing matches what will actually print on that sheet.

PDF Export

NullCad exports drafting sheets as a multi-page vector PDF — geometry is written as real vector paths (and text as glyph outlines), not a raster image, so the output stays crisp at any zoom and prints cleanly.

What gets exported

Each page proxy in the document becomes one PDF page:

  1. Page content — geometry sharing the page’s phase and intersecting its bounds is transformed from world meters to paper millimeters at the page’s draw scale, styled per layer (color, line weight, line type), and clipped to the sheet.
  2. Viewport content — geometry composed through any viewports on the page, from whichever phase each viewport targets, at that viewport’s own pan/zoom/rotation.
  3. Sheet template — the assigned title block, rendered on top, with its substitutable fields (<PROJECT>, sheet number/name, etc.) filled in.

Pages are ordered by (sheet number, name), so legacy documents (where every sheet number is 0) keep their existing name order.

Exporting

Open the PDF export dialog from the Sheet Manager or File ▸ Export ▸ PDF. You can choose which pages to include and export options such as monochrome output.

Line types in the exported PDF

Custom linetype patterns (dashes, dots, center lines, etc.) are resolved against the project’s linetype library and interpreted as paper millimeters, independent of the page’s draw scale — so a dash pattern defined for a 1:500 sheet looks the same physical size as the same pattern on a 1:100 sheet.

Text

Text is rendered as filled glyph outlines rather than PDF text objects, so the exported PDF doesn’t depend on font availability on the viewer’s system and renders identically everywhere.

Supported Formats

Native (pure Rust — always available)

FormatExtensionNotes
ESRI Shapefile.shpIndustry-standard GIS vector format
GeoJSON.geojson, .jsonWeb-friendly geographic data format; non-finite coordinates are rejected on import
DXF.dxfAutoCAD Drawing Exchange Format, with native Circle/Arc support; non-finite coordinates are rejected on import
CSV.csvPoint data with configurable column mapping (import) and column/format selection (export) — see CSV Export

GDAL (optional — requires the gdal feature)

FormatExtensionNotes
KML.kmlKeyhole Markup Language, for Google Earth
GeoPackage.gpkgModern SQLite-based GIS format
MapInfo TAB.tabMapInfo native format
OpenFileGDB.gdbESRI File Geodatabase

See Building from Source for how to build with GDAL support.

Coordinate reference systems

All formats support CRS handling with automatic transformation options via proj4rs.

DXF import notes

  • BYBLOCK colors fall back to black (the source block’s actual color isn’t always recoverable through the parsing crate used).
  • When importing DXF sheet templates (paper-space layouts, for use as drafting title blocks), the true physical paper size is recovered from the DXF’s plot-settings where possible; binary DXF files fall back to a layout-limits/content-extents heuristic.

Web viewer bundle (.ncweb)

A separate, purpose-built export format for the companion field/phone viewer — see Web Viewer Bundle.

CSV Export

NullCad supports exporting geometry to CSV with customizable column options, complementing CSV import.

Column selection

ColumnDescription
NamePoint number or label (auto-generated as PT1, PT2, … if empty)
Vertex IndexIndex of a vertex within a multi-vertex geometry (lines/polylines)
XEasting or longitude
YNorthing or latitude
Z / HeightElevation or height (defaults to 0 if unset)
CodeFeature code for categorization
DescriptionDetailed description or notes
LayerLayer name

Format options

  • Coordinate precision: 0–10 decimal places, defaults to 6.

Geometry handling

By default, only Point geometry is exported.

Enabling export non-point geometries adds:

  • Lines — all vertices (start and end), one row each.
  • Polylines — all vertices, one row each.
  • Polygons — the centroid, as a single row.
  • Circles, Arcs, Rectangles — the center point, as a single row.
  • Text — the insertion point, as a single row.

Export behavior

  • Selection-aware: if geometry is selected, only the selection is exported; otherwise all active (non-deleted) geometry is exported.
  • Auto-generated names: points with an empty name get PT1, PT2, etc.
  • Default height: Z/Height defaults to 0 (rather than being left empty) when unset.
  • Multiple rows per geometry: exporting a line/polyline with export of non-points enabled produces one row per vertex, all sharing the same metadata (name, code, description, …).

Usage

  1. Select geometry to export (optional — leave nothing selected to export everything).
  2. File ▸ Export ▸ CSV…
  3. Choose which columns to include, and optionally enable “Export non-point geometries.”
  4. Export, then choose the output file location.

Default configuration

Enabled by default: Name, X, Y, Z/Height, Code, Description, 6 decimal places of precision.

Disabled by default: Vertex Index, Layer, non-point geometry export.

CSV format

Standard RFC 4180: header row, comma-separated fields, fields containing commas are quoted, empty fields are unquoted empty strings.

Example — points only (default)

name,x,y,z,code,description
PT1,500000.123456,4500000.654321,123.450000,CTRL,Control Point
PT2,500100.234567,4500100.765432,124.560000,BNDRY,Boundary Marker
PT3,500200.345678,4500200.876543,0.000000,TOPO,Topographic Point

Example — line vertices with vertex index

name,vertex_index,x,y,code
LINE1,0,100.000000,200.000000,BOUND
LINE1,1,150.000000,250.000000,BOUND
LINE1,2,200.000000,300.000000,BOUND

Example — auto-generated names

name,x,y,z
PT1,100.000,200.000,0.000
PT2,300.000,400.000,0.000

See also

Web Viewer Bundle

NullCad can export a self-contained .ncweb bundle for a companion browser/phone viewer — useful for reviewing a survey project in the field without running the full desktop application.

What’s in the bundle

.ncweb is a ZIP archive containing:

  • manifest.json — CRS, extent, camera position, layer list.
  • features.geojson — geometry as GeoJSON, with styles resolved (colors, line weights, etc.) at export time so the viewer doesn’t need to replicate NullCad’s full styling logic.
  • resources/ — embedded resources (e.g. reference images) needed by the viewer.

Exporting

File ▸ Export ▸ Web Viewer Bundle…, then choose an output location for the .ncweb file.

The viewer

The companion viewer is a TypeScript Progressive Web App (PWA), designed for field/phone use — it opens a .ncweb bundle and renders the exported geometry with the resolved styles, without needing network connectivity once loaded.

Keeping exporter and viewer in sync

The .ncweb format is produced by the Rust exporter (src/gis_io/native/web_bundle.rs) and consumed by the separate viewer/ TypeScript project. Because the format is shared between two independently built codebases, changes to the bundle’s schema need to land on both sides together.

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).

MCP Server

NullCad includes an in-process Model Context Protocol (MCP) server (HTTP+SSE), letting an AI agent read and create geometry in the currently open document.

How it works

The MCP server runs as a background tokio task inside the application. Mutations requested by an agent are funnelled back to the egui main thread via a channel, so they go through the same operation system as any other edit — every agent-driven change is undoable and recorded in the audit trail, exactly like Lua plugin edits or manual drawing.

Enabling the server

The MCP server’s settings (enable/disable, port) are available in the application settings and persist with the document.

Connecting an agent

Once enabled, point an MCP-compatible client at the server’s HTTP+SSE endpoint to give an agent access to:

  • Reading document info, layers, and geometry.
  • Creating and modifying geometry (points, lines, polylines, polygons, circles, arcs, rectangles, text).
  • Reading and changing the current selection.

The tool surface mirrors the Lua plugin API’s ctx methods (see Lua Plugins) — both are implemented against the same underlying operation builders (mcp/tools.rs), so behavior (units, coordinate conventions, rotation direction) is consistent between the two integration paths.

Why in-process?

Running the MCP server inside the application (rather than as a separate process talking to a file) means an agent is always working against the live, in-memory document state — including unsaved changes — and its edits appear immediately in the UI, just like a human’s.

The .nullcad Archive

NullCad projects are saved as a ZIP-based archive format (.nullcad), containing:

manifest.json     — project metadata, resource index, phase status
document/         — geometry, layers, document metadata, comparisons,
                     selection sets
operations/       — complete operation history
state/            — camera positions, snap config, application settings
resources/        — embedded files (PDFs, images, field data)

Why an archive, not a single JSON file?

Bundling resources (reference PDFs, images, field data files) directly in the project file means a .nullcad file is fully self-contained — moving, copying, or archiving one file preserves everything needed to reopen the project exactly as it was, without broken links to external files.

Legacy JSON format

Older NullCad projects were saved as plain JSON. These are automatically detected and migrated on load — you don’t need to convert them manually.

Operation history as audit trail

The operations/ directory holds the complete, append-only operation log for the document (see Operation System) — every mutation ever made, in order, including the ones later undone. This gives every .nullcad file a full audit trail of how the survey model was built, not just its final state.

Resource management

Plans, photos, and field data files can be embedded directly in the project archive under resources/ and referenced from geometry or annotations — e.g. a PlanImage geometry object placing a reference PDF page in world space, or a resource attached to a point for field documentation.

  • .ncweb — a separate, purpose-built bundle for the companion web viewer. See Web Viewer Bundle.

Precision Handling

Cadastral surveying works in large coordinate systems — a typical UTM Easting/Northing looks like 500,000m / 5,000,000m. Rendering that directly on the GPU is a precision problem: a 32-bit float (f32) at 1,000,000m has roughly 0.125m of representable precision, which is useless for millimeter-accuracy survey work.

The solution: camera-relative rendering in f64

  1. All geometry is stored in f64 world coordinates.

  2. Camera position is tracked in f64 — handles coordinates like 500,000m without loss.

  3. Before rendering, coordinates are transformed to camera-relative space, in f64:

    #![allow(unused)]
    fn main() {
    vertex_camera = vertex_world - camera_position
    }
  4. Only camera-relative coordinates are converted to f32, right before upload to the GPU vertex buffer.

Because camera-relative coordinates are small (bounded by whatever’s actually visible on screen), f32 precision is excellent for them — even though the original world coordinates were far too large for f32 to represent precisely.

Worked example

A point at (500000.001, 5000000.001) with the camera at (500000, 5000000) becomes (0.001, 0.001) in camera-relative space — small enough for f32 to represent exactly.

Rule of thumb

Never apply the camera position transform in a shader. Do the world-to-camera-relative subtraction in f64 on the CPU first, then hand the already-small f32 values to the GPU. Applying it in the shader (in f32) reintroduces the precision loss the whole scheme exists to avoid.

Where this matters in the codebase

  • renderer/mod.rs — the wgpu pipeline that performs the camera-relative transform.
  • renderer/camera.rsCamera2D, tracked in f64.
  • Every geometry type in geometry/ stores its coordinates as f64.

Operation System

Every mutation to a NullCad document — drawing, moving, deleting, vertex edits, layer changes, comparisons, and more — goes through a single, strongly-typed operation system. This is what powers undo/redo, the audit trail, and (indirectly) the Lua plugin and MCP server integrations, since both reuse the same operation builders rather than mutating the document directly.

Key pieces

  • OperationType — a strongly-typed enum covering every mutation: create/delete/restore, move/rotate/scale, vertex edits, split/extend/trim, explode, layer changes, comparisons, and more.
  • OperationHistory — an append-only log. Undo doesn’t remove the original operation — it appends a compensating (inverse) operation. Redo re-applies. This means the full history is always preserved as an audit trail, and a new operation clears the redo stack (no branching history).
  • DocumentState — combines a Document, an OperationHistory, and a Vec<Snapshot>. apply_operation() applies the operation, records it, and auto-snapshots periodically (every 50 operations by default). undo()/redo() create and apply the compensating operations.
  • Phase tracking — every operation is tagged with the active survey phase at the time it was applied.

Example

#![allow(unused)]
fn main() {
let op = OperationType::MoveGeometry {
    geometry_ids: vec![id],
    delta: Vector2::new(10.0, 20.0),
};
doc_state.apply_operation(user_id, op)?;
doc_state.undo(user_id)?;   // applies the compensating op
doc_state.redo(user_id)?;
}

Soft deletes

Deleted geometry is tombstoned rather than physically removed, preserving history — a delete is just another operation with a well-defined inverse (restore).

Persistence

Operation history is saved with the document (see The .nullcad Archive), so the full edit history travels with the project file.

Collaboration-ready by design

The operations system was designed to support future collaborative editing with minimal changes: UUID geometry IDs, tombstone soft deletes, operation metadata (user_id/timestamp/phase/description), an append-only log, and a HashMap-based document are all CRDT-friendly properties. The remaining path to multi-user editing is real authenticated user IDs, an operation-sync transport, conflict resolution, and per-user undo — without needing to break the existing model.

Adding a new operation

  1. Add a variant to OperationType in src/operations/types.rs.
  2. Implement inverse(), summary(), and affected_geometry_ids() for it.
  3. Add execution logic in apply_operation_type() in src/operations/executor.rs.
  4. Apply it via DocumentState::apply_operation().
  5. Test the operation, its inverse, and its summary.

Module Structure

The application state lives on NullCadApp, split across several app_* files by concern (canvas, dialogs, menus, tools, properties, layers, status bar, input, I/O), backed by a set of focused subsystem modules.

ModulePurpose
app.rsMain application state and egui integration
app_canvas_ui.rs, app_dialogs_ui.rs, app_menu_ui.rs, app_tool_ui.rs, app_properties_ui.rs, app_layer_ui.rs, app_statusbar_ui.rs, app_input.rs, app_io.rsUI and input, split by concern
document/Document model, geometry objects, spatial index, comparisons, selection sets
operations/Operation types, history, executor, snapshots — see Operation System
commands/Legacy command system (superseded by operations/, still present, being migrated out)
geometry/Geometry primitives (Point, Line, Polyline, Polygon, Rectangle, Circle, Arc, Text, PlanImage, TIN surface, page proxy, viewport)
renderer/wgpu renderer, camera, auto-contrast, line types, PDF/image underlays, tile rendering — see Precision Handling
tools/Interactive tool modules (point, edit, trim, offset, text/callout annotation, TIN, page proxy, viewport)
annotations/Text and callout annotations with anchoring, styling, and leaders
labels/Point label generation with collision detection
selection/, select_similar.rsSelection set management, hit testing, select-by-attribute
snapping/Snap point detection and configuration
layers/, codelist/Layer/group management and code→layer import mapping
gis_io/native/Pure-Rust import/export: Shapefile, GeoJSON, DXF, CSV, .ncweb web bundle
gis_io/gdal/Optional GDAL formats: KML, GeoPackage, MapInfo TAB, FileGDB
pdf_export/, pdf_viewer.rsVector PDF export (page proxies → pages) and in-app PDF plan viewer
mcp/In-process MCP server exposing the document to AI agents
plugins/Lua plugin system: commands, tools, panels/dialogs, event hooks
console/Application message log
settings/, autosave.rs, template.rsPersistent settings, autosave/crash recovery, document templates
ui/Reusable UI components (operation log, phase tabs, resource panel, color/codelist pickers)
archive/ZIP-based .nullcad file reading/writing with resource management
utils/DMS parsing, unit conversions

Document model (src/document/)

  • Document — the central store; geometry in a HashMap keyed by GeometryId.
  • GeometryId — UUID v4, for global uniqueness (collaboration-ready).
  • GeometryObject — wraps a Geometry with metadata (created_by, timestamps, phase, layer, name/code, tombstone).
  • Spatial index — an rstar R-tree, auto-maintained on create/delete/modify and rebuilt on load; powers O(log n) hit testing and range queries.

Geometry (src/geometry/)

The Geometry enum covers Point, Line, Polyline, Polygon, Text, Circle, Arc, Rectangle, PlanImage (a PDF page placed in world space), TinSurface, PageProxy, Viewport, and ViewportView. All coordinates are f64; the crate integrates with the geo crate’s types.

Modular tool development

Interactive tools are self-contained modules in src/tools/, each encapsulating its own state and behavior, which keeps the app_* files thin and lets tools be unit-tested in isolation.

A tool module typically provides:

  • A state struct (new(), reset()).
  • A tool struct holding that state, with name(), instructions(), handle_click(...) -> (Option<OperationType>, bool) (the bool = should-exit), render_ui(...), and reset().

To add a new tool:

  1. Add pub mod my_tool; in src/tools/mod.rs and a variant to the Tool enum (plus its name()/instructions() arms).
  2. Add a tool instance field to NullCadApp and initialize it.
  3. Dispatch clicks and UI rendering from the app_* files based on the active tool.
  4. Add a toolbar button that sets the active tool and calls reset().

src/tools/point_tool.rs is the reference implementation. Best practices: keep all tool logic in the module, route every mutation through DocumentState::apply_operation(), reset tool state on activation, provide clear instructions() text, handle edge cases (invalid input, no snap point), and unit-test the module.

Adding a new geometry type

  1. Add the type in src/geometry/ using f64 coordinates.
  2. Add a variant to the Geometry enum in geometry/mod.rs.
  3. Handle it in the renderer’s vertex generation.
  4. Handle it everywhere else the Geometry enum is matched (hit testing, serialization, export paths, MCP tools.rs, etc.) — the compiler will point these out.

Camera controls (renderer/camera.rs)

  • Pan: middle mouse drag (PAN_SENSITIVITY, default 0.005), zoom-scaled.
  • Zoom: scroll wheel toward cursor; clamp range ~0.001–10000.
  • Position lives in f64; the view-projection matrix handles only zoom/aspect, never translation (see Precision Handling).

Keyboard Shortcuts & Controls

A full, searchable list is available in-app via the keyboard shortcuts help dialog. This page summarizes the defaults.

ActionInput
PanMiddle mouse drag
ZoomScroll wheel (zooms toward cursor)
Reset cameraReset Camera button
Distraction-free modeF11
View bookmarks1–9 to recall, Ctrl+1–9 to set

Selection

ActionInput
SelectClick on geometry
Multi-selectCtrl+Click
Rectangle selectClick and drag on empty canvas
Select allCtrl+A

Editing

ActionInput
UndoCtrl+Z
RedoCtrl+Y or Ctrl+Shift+Z
Delete selectedDelete
ExplodeB
Transform gizmo (move/rotate selection)W
Toggle consoleF12

Tool categories

CategoryTools
SelectDefault selection mode
DrawPoint, Line, Polyline, Leader, Polygon, Text, Callout, Circle, Arc, Rectangle
ModifyMove, Rotate, Scale, Trim/Extend, Offset, Transform Gizmo
SurveyTraverse, Comparison
MeasureDistance and bearing measurement (bearing shown in DMS, rounded to the nearest second)
Surface & DraftingTIN Surface, Page Proxy, Viewport

See also: User Guide for what each tool does.