Ranger architecture

2 — How a dropped file becomes a picture

Written: 2026-08-15 · Established by: reading src/parser/index.ts, src/parser/tokenizer.ts, src/parser/interpreter.ts, src/checks/index.ts, src/store/file-slice.ts, src/platform/nativeFileAccess.ts and src/platform/useNativeFileEvents.ts at commit 6052e10.

This is the path everything else hangs off. A file arrives, parseGCode turns its text into a GCodeFile, the store holds that one object, and the viewer and the sidebar are both projections of it. Nothing is cached between the text and the picture, so re-parsing is how every later change lands too — see diagram 4.

Intake and fan-out. The store holds one GCodeFile, and three surfaces are projections of it. Two shapes of intake feed the same loadFile: a browser File object, read client-side, or — under Tauri only — a path read on the Rust side and handed over already as text, since there is no File object for a path the OS gave the app rather than a <input> element.

Drop zone
or file picker

readFileAsText

loadFile

Native open, drop
or launch (Tauri only)

read_native_file
(Rust command)

parseGCode

GCodeFile

Per-concern resets

Viewer

CodeMirror

Sidebar panels

native is src/platform/nativeFileAccess.ts's openFilePicker() (the File menu's Open, and the header's Open button) plus src/platform/useNativeFileEvents.ts's two live subscriptions (Finder/Explorer drag-and-drop, and the .nc/.ngc file a launch was opened with) — three call sites, one Rust command, both converging on the same loadFile the browser path already used.

Inside parseGCode. tokenize is the only stage that reads the raw text; every other stage reads blocks, and three of them read the interpreted moves as well.

raw text

tokenize

interpretBlocks

detect* and parse*
units, subprograms, cycles
WCS, tool table, G10

splitIntoOperations

runChecks

GCodeFile

The order in parseGCode is the contract

tokenize is the only thing that reads the raw text. Everything downstream reads GCodeBlock[], and three of the six stages read the interpreted Move[] as well. That split is why a check can report a line number without re-lexing, and why src/transforms/apply.ts can splice text back by block index.

interpretBlocks is where the program is actually executed. It carries modal state, expands subprogram calls and canned cycles, and returns the four things the rest of the parse needs: the moves, the block index it halted at if it could not continue, the calls whose expansion hit the cap, and the cycle it declined. Those last three exist so runChecks can report a limit as a finding rather than the parser failing silently.

Halting is normal and partial. haltedAtBlockIndex is a real state a production file reaches — issues #291, #293 and #296 are all programs that halt on a construct Ranger does not yet interpret. The moves found before the halt are still drawn.

Two invariants this path enforces that the diagram cannot show

Position known-ness is per axis. A move that commands an axis never established is kept with fromAssumed: true rather than dropped, and anything turning moves into geometry, bounds or a count of real motion filters on isDrawable — ADR-0016.

Units are 'mm' | 'inch' | null, and null is not millimetres — ADR-0021. detectUnits returns the third value for a program that declares neither, which is a program that runs in whatever the control was left in.

Where the second entry point is

safeParseGCode in src/parser/index.ts wraps the same call in a try. The store's loadFile has its own private tryParse doing the same thing, because a caller outside the store — a second file picked for comparison in src/compare/ — has no undo history to fall back on if the parse throws.

Drag to pan · scroll to zoom · Esc to close