Repository navigation
Adding a G code command
The interpreter doesn't hardcode its command set. Every supported gcode is an entry in a registry Map pointing at a small handler module, and adding a command is a four-file change: write a handler, export it, register it, test it. This page is the end-to-end recipe, plus the conventions that keep handlers consistent.
The registry lives in src/interpreter.ts:
export type CommandHandler = (command: GCodeCommand, job: Job) => void;
export const handlers: ReadonlyMap<string, CommandHandler> = new Map([
['g0', linearMove],
['g1', linearMove],
['g2', arcMove],
// …
]);Interpreter.execute() looks each command up by its lowercase gcode and calls the handler if one exists — commands without an entry are silently ignored, which is how the library copes with the enormous surface of real-world G-code without special-casing it.
Three design points worth knowing before you write a handler:
-
Everything a handler needs lives on the
Job. State, stats, and path bookkeeping (breakPath,resolvePosition) are job methods, so handlers are plain functions with no interpreter dependency. Handlers import theCommandHandlertype with a type-only import, so registering them ininterpreter.tscreates no circular runtime dependency. -
One handler can serve many gcodes.
linearMovehandles bothg0andg1;selectToolreads the tool number out ofcommand.gcodeitself. The registry decides which spellings exist. -
Configurable handlers use a factory.
makeArcMove(options)builds an arc handler around its own tessellator; theInterpreterconstructor swaps theg2/g3entries in a copied map whenarcChordToleranceis set. If your handler needs configuration, follow that pattern rather than module-level state.
By the time a handler runs, the parser has normalized the command, and two guarantees simplify every handler:
-
Params are lowercase and finite.
X10.5arrives asparams.x === 10.5; a malformed value likeX1e999orXabcwas dropped at the parse boundary, never passed through asNaNorInfinity. Handlers therefore usex ?? state.xfreely with no finite-ness checks. -
The gcode word is normalized.
G01becomes'g1'(the number passes throughNumber(), collapsing leading zeros),T3becomes't3'. Register the normalized spelling only.
Say you're adding support for command GXX.
1. Write the handler — src/interpreter/commands/my-command.ts:
import type { CommandHandler } from '../../interpreter';
/**
* Executes a GXX command
* @param command - GCodeCommand containing the command
* @param job - Job instance to update
*/
export const myCommand: CommandHandler = (command, job) => {
// read command.params, mutate job.state / paths / stats
};Pick the existing handler closest to your command as a template:
| Your command… | Template | Why |
|---|---|---|
| only changes state | set-units.ts |
two-liner, the minimal shape |
| moves the nozzle | linear-move.ts |
the full move pattern: path breaking, stats, bounding box |
| derives data from the gcode word | select-tool.ts |
parses command.gcode itself |
| needs configuration | arc-move.ts |
the make* factory pattern |
2. Export it — add one line to src/interpreter/commands/index.ts:
export { myCommand } from './my-command';3. Register it — add the entry (or entries) to the handlers map in src/interpreter.ts:
['gXX', myCommand],4. Test it — src/__tests__/interpreter/commands/my-command.ts. The convention is to test the handler directly, no parser or interpreter in the loop: construct a GCodeCommand, run the handler against a fresh Job, assert the mutation. From the real set-units test:
import { test, expect } from 'vitest';
import { GCodeCommand } from '../../../parser/gcode-parser';
import { setInchUnits } from '../../../interpreter/commands';
import { Job } from '../../../job';
test('G20 sets the units to inches', () => {
const command = new GCodeCommand('G20', 'g20', {});
const job = new Job();
setInchUnits(command, job);
expect(job.state.units).toEqual('in');
});test.each covers gcode families nicely (see the select-tool test running T0–T7). And note the coverage bar: the repo enforces 100% per-file coverage for everything under src/ by default, so every branch in your handler needs a case — run npm test and the threshold failure will tell you what you missed.
These aren't style preferences; each one traces to a bug that shipped once.
-
??, never||, for coordinate fallbacks.X0is a legitimate coordinate, andx || state.xtreats it as "not given". This is safe only because the parser drops non-finite params — don't reintroduce||as a NaN guard. -
e > 0means extrusion. Exactly that. A negative E is a retraction, which is a travel move — the loosere ?check once rendered retracting arcs as phantom filament. If your command moves the nozzle, matchlinearMove's classification and itsbreakPathcall when the type flips. -
Resolve before you draw. Axes are
undefineduntil homed. Write state first (state.x = x ?? state.x), then takejob.resolvePosition()(un-homed axes resolve to 0) for the point you add to the path. Never put rawstate.xinto geometry. -
Only extrusion grows the bounding box. Travel moves can wander far outside the print; calling
boundingBox.update()on them breaks camera framing. -
Zero-length moves are counted, not drawn. A command with no X/Y/Z still ticks the right
job.statscounter (retraction, feedrate change, …). Keep the stats honest — the dev GUI and tests read them. -
No side effects outside the
Job. Handlers run during parsing/streaming, possibly thousands of times per second, on files of arbitrary origin. No logging per command, no DOM, no rendering — the renderer reads the job later.
Several currently-unsupported commands (the positioning family around G90/G91/M82/M83, G92 offsets and friends) have open issues with agreed designs — search the issue tracker before implementing, so your handler lands on the planned semantics rather than inventing parallel ones.
| What | Where |
|---|---|
The registry and CommandHandler type |
src/interpreter.ts |
| Handler modules | src/interpreter/commands/ |
| Handler tests | src/__tests__/interpreter/commands/ |
| The parser guarantees | src/parser/gcode-parser.ts |
The job toolbox (breakPath, resolvePosition, stats) |
src/job.ts |