# Backend — Commands

**Location:** src-tauri/src/commands/

<span>Tauri commands are Rust functions exposed to the frontend via </span>**invoke**. All commands are registered in and live in the module. Each page below covers one file.

# rover_state.rs — Rover Mode State

<p class="callout warning">The state described here MIGHT be subject to change</p>

<span style="white-space: pre-wrap;">Manages the three global boolean flags that track the rover's current operating mode. The state is held in a </span>`<span class="editor-theme-code">RoverState</span>`<span style="white-space: pre-wrap;"> struct registered as Tauri managed state (initialised in </span>`<span class="editor-theme-code">lib.rs</span>`), so it persists for the lifetime of the application.

### State fields

<table id="bkmrk-statedefaultdescript"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><td>**State**

</td><td>**Default**

</td><td>**Description**

</td></tr><tr><td>`<span class="editor-theme-code">drive_manual_mode</span>`

</td><td>`<span class="editor-theme-code">true</span>`

</td><td>Whether the rover driving is in manual control

</td></tr><tr><td>`<span class="editor-theme-code">arm_manual_mode</span>`

</td><td>`<span class="editor-theme-code">true</span>`

</td><td>Whether the arm is in manual control

</td></tr><tr><td>`<span class="editor-theme-code">pickup_mode</span>`

</td><td>`<span class="editor-theme-code">false</span>`

</td><td>Whether the rover is in driving or pickup mode

</td></tr></tbody></table>

### Commands

**`<strong class="editor-theme-bold editor-theme-code">get_state(state_type: StateType) → bool</strong>`**<span style="white-space: pre-wrap;"> Returns the current value of the requested state flag. Called on page mount to sync the UI with the actual rover state.</span>

**`<strong class="editor-theme-bold editor-theme-code">set_state(state_type: StateType, value: bool)</strong>`**<span style="white-space: pre-wrap;"> Sets a state flag. Called by the frontend when the operator switches modes (e.g. toggling pick-up mode on the dashboard).</span>

`<span class="editor-theme-code">StateType</span>`<span style="white-space: pre-wrap;"> is an enum with variants: </span>`<span class="editor-theme-code">DriveManual</span>`<span style="white-space: pre-wrap;">, </span>`<span class="editor-theme-code">ArmManual</span>`<span style="white-space: pre-wrap;">, </span>`<span class="editor-theme-code">Pickup</span>`.

# file_management.rs — Persistent File Storage

<span style="white-space: pre-wrap;">Handles all file I/O for the application. Files are stored inside Tauri's </span>`<span class="editor-theme-code">app_data_dir</span>`<span style="white-space: pre-wrap;">, which is platform-specific (e.g. </span>`<span class="editor-theme-code">%APPDATA%\base_station</span>`<span style="white-space: pre-wrap;"> on Windows). Three subdirectories are used:</span>

<table id="bkmrk-directorypurposetask"><colgroup><col style="width: 240px;"></col><col style="width: 240px;"></col></colgroup><tbody><tr style="height: 11px;"><td>**Directory**

</td><td>**Purpose**

</td></tr><tr><td>`<span class="editor-theme-code">tasks/</span>`

</td><td>Saved task plan files

</td></tr><tr><td>`<span class="editor-theme-code">images/</span>`

</td><td>Snapshots captured from video feeds

</td></tr><tr><td>`<span class="editor-theme-code">maps/</span>`

</td><td>Imported map files

</td></tr></tbody></table>

<span style="white-space: pre-wrap;">These directories are created automatically on startup by </span>`<span class="editor-theme-code">ensure_storage_dirs_internal()</span>`.

### Commands

**`<strong class="editor-theme-bold editor-theme-code">save_task_file(file_name, data, directory)</strong>`**<span style="white-space: pre-wrap;"> Writes raw bytes to a file in the given subdirectory. Used to persist task plans.</span>

**`<strong class="editor-theme-bold editor-theme-code">list_task_files(directory) → Vec<String></strong>`**<span style="white-space: pre-wrap;"> Returns a list of filenames in the given subdirectory. Returns an empty list if the directory doesn't exist yet.</span>

**`<strong class="editor-theme-bold editor-theme-code">read_task_file(file_name) → Vec<u8></strong>`**<span style="white-space: pre-wrap;"> Reads a file from the </span>`<span class="editor-theme-code">tasks/</span>`<span style="white-space: pre-wrap;"> directory and returns its raw bytes.</span>

**`<strong class="editor-theme-bold editor-theme-code">delete_one_file(directory, file_name)</strong>`**<span style="white-space: pre-wrap;"> Deletes a single named file from the given subdirectory. Does nothing if the file doesn't exist.</span>

**`<strong class="editor-theme-bold editor-theme-code">delete_all_task_files(directory)</strong>`**<span style="white-space: pre-wrap;"> Removes all files in a subdirectory by deleting and recreating it.</span>

**`<strong class="editor-theme-bold editor-theme-code">import_map_file(directory)</strong>`**<span style="white-space: pre-wrap;"> Copies a file from an arbitrary path on the filesystem into the </span>`<span class="editor-theme-code">maps/</span>`<span style="white-space: pre-wrap;"> subdirectory. Used when the operator imports a new map via the file picker. The original filename is preserved.</span>

**`<strong class="editor-theme-bold editor-theme-code">save_snapshot(port, file_name)</strong>`**<span style="white-space: pre-wrap;"> Captures a single JPEG frame from an MJPEG stream (given by its localhost URL/port) and saves it to the </span>`<span class="editor-theme-code">images/</span>`<span style="white-space: pre-wrap;"> directory as </span>`<span class="editor-theme-code">{file_name}.jpg</span>`. Used in the Science task to photograph samples. It scans the raw HTTP stream for the JPEG start marker (`<span class="editor-theme-code">0xFF 0xD8</span>`) and end marker (`<span class="editor-theme-code">0xFF 0xD9</span>`), extracting the first complete frame. Has a 5 MB safety limit per frame.

# gstreamer.rs — Video Streaming

<span style="white-space: pre-wrap;">Receives H.264 video from the rover over UDP, decodes it, and serves it as MJPEG over HTTP so the frontend can display it in </span>`<span class="editor-theme-code"><img></span>`<span style="white-space: pre-wrap;"> tags.</span>

### Pipeline per camera

```
udpsrc (UDP port) → rtpjitterbuffer → rtph264depay → avdec_h264 → videoconvert → jpegenc → appsink
```

<span style="white-space: pre-wrap;">Each decoded JPEG frame is placed into a shared </span>`<span class="editor-theme-code">FrameBuffer</span>`<span style="white-space: pre-wrap;"> (</span>`<span class="editor-theme-code">Arc<Mutex<Option<Bytes>>></span>`<span style="white-space: pre-wrap;">). A separate async HTTP server (using </span>`<span class="editor-theme-code">warp</span>`<span style="white-space: pre-wrap;">) reads from that buffer and streams it as </span>`<span class="editor-theme-code">multipart/x-mixed-replace</span>`<span style="white-space: pre-wrap;"> — the standard MJPEG format.</span>

### Port mapping

<table id="bkmrk-udp-input-porthttp-o"><colgroup><col style="width: 240px;"></col><col style="width: 240px;"></col><col style="width: 240px;"></col></colgroup><tbody><tr><td>**UDP input port**

</td><td>**HTTP output port**

</td><td>**Camera**

</td></tr><tr style="height: 10px;"><td>4500

</td><td>5000

</td><td>Depth / front camera

</td></tr><tr><td>4501

</td><td>5001

</td><td>Secondary camera

</td></tr><tr><td>4502

</td><td>5002

</td><td>Arm camera

</td></tr></tbody></table>

### Feed health monitoring

A background task (`<span class="editor-theme-code">watch_feed_health</span>`<span style="white-space: pre-wrap;">) polls each stream every 500ms. If no frame has been received within 2 seconds, the stream is considered stale. The backend emits a </span>`<span class="editor-theme-code">camera-feed-status</span>`<span style="white-space: pre-wrap;"> Tauri event to the frontend with the payload:</span>

```json
{ "port": 5000, "stale": true }
```

The frontend listens for this event to show feed status indicators.

<p class="callout info">**For development without rover hardware**<span style="white-space: pre-wrap;">, run the </span>`<span class="editor-theme-code">fake_camera_gstreamer</span>`<span style="white-space: pre-wrap;">. For instructions see </span>[Common Operations](https://bookstack.roboteamtwente.nl/link/159#bkmrk-video-feeds).</p>

# network.rs — UDP & Dummy Simulator

<p class="callout warning">This file is work in progress</p>

Exposes commands related to the UDP connection and the development simulator.

### Commands

**`<strong class="editor-theme-bold editor-theme-code">send_ping_cmd(packet_type)</strong>`**<span style="white-space: pre-wrap;"> Intended to send a ping packet to the rover over UDP. Currently logs to console — full implementation pending.</span>

**`<strong class="editor-theme-bold editor-theme-code">start_dummy_streams()</strong>`**<span style="white-space: pre-wrap;"> Starts the full multi-stream simulator. Sends fake protobuf packets to </span>`<span class="editor-theme-code">127.0.0.1:9000</span>`<span style="white-space: pre-wrap;"> mimicking live rover data. Useful for UI development without hardware. Simulator config:</span>

- Jitter: 30ms
- Simulated packet loss: 2%

**`<strong class="editor-theme-bold editor-theme-code">start_detection_sim()</strong>` Starts a single stream for the detection object stream dummy data.

**`<strong class="editor-theme-bold editor-theme-code">stop_dummy_streams()</strong>`**<span style="white-space: pre-wrap;"> Signals the running simulator to stop by setting a shared cancellation flag.</span>

<span style="white-space: pre-wrap;">The </span>`<span class="editor-theme-code">DummyStreamHandle</span>`<span style="white-space: pre-wrap;"> struct (managed Tauri state) holds the cancellation handle so any command can stop the simulator.</span>

# controller.rs — Gamepad Input

<span style="white-space: pre-wrap;">Runs a background listener for gamepad input using the </span>`<span class="editor-theme-code">gilrs</span>`<span style="white-space: pre-wrap;"> library, translating button and axis events into UDP packets sent to the rover. All dispatching is gated on the current mode (drive vs. pickup) and whether the relevant manual mode is active.</span>

### Modes

<span style="white-space: pre-wrap;">The controller operates in one of two top-level modes, toggled with the </span>**Start**<span style="white-space: pre-wrap;"> button:</span>

<table id="bkmrk-modeactive-whencontr"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>Mode

</th><th>Active when

</th><th>Controls

</th></tr><tr><td>Drive

</td><td>`<span class="editor-theme-code">pickup_mode = false</span>`

</td><td>Left stick (forward/turn), triggers (brake)

</td></tr><tr><td>Pickup

</td><td>`<span class="editor-theme-code">pickup_mode = true</span>`

</td><td>Both sticks (X/Y/rotate/flick), D-pad (Z), triggers (gripper)

</td></tr></tbody></table>

<span style="white-space: pre-wrap;">Within each mode, the </span>**Select**<span style="white-space: pre-wrap;"> button toggles the relevant manual mode (</span>`<span class="editor-theme-code">drive_manual_mode</span>`<span style="white-space: pre-wrap;"> or </span>`<span class="editor-theme-code">arm_manual_mode</span>`). Commands are silently suppressed when the associated manual mode is inactive.

### Threads

Three concurrent threads are spawned on startup:

`<span class="editor-theme-code">start_controller_listener()</span>`<span style="white-space: pre-wrap;"> — Entry point. Spawns all threads and owns the shared </span>`<span class="editor-theme-code">CommandState</span>`.

**Event thread**<span style="white-space: pre-wrap;"> — Polls </span>`<span class="editor-theme-code">gilrs</span>`<span style="white-space: pre-wrap;"> at ~125 Hz (8 ms sleep) and routes each </span>`<span class="editor-theme-code">Event</span>`<span style="white-space: pre-wrap;"> to the appropriate handler. Holds the </span>`<span class="editor-theme-code">shared</span>`<span style="white-space: pre-wrap;"> mutex only for the duration of each state update.</span>

**Heartbeat thread**<span style="white-space: pre-wrap;"> — Wakes every 2 seconds and re-sends the current state (drive axes + brake, or arm + brake). Ensures the rover never silently drifts from its commanded state if packets are dropped.</span>

**Ramp threads**<span style="white-space: pre-wrap;"> — Spawned on demand when a ramped button (D-pad up/down, left/right trigger in pickup mode) is pressed. Ticks at ~60 Hz and increments the axis value from </span>`<span class="editor-theme-code">0.0</span>`<span style="white-space: pre-wrap;"> toward </span>`<span class="editor-theme-code">±1.0</span>`<span style="white-space: pre-wrap;"> over </span>`<span class="editor-theme-code">RAMP_DURATION_SECS</span>`<span style="white-space: pre-wrap;"> (1.0 s). The thread exits when the direction is set back to </span>`<span class="editor-theme-code">0.0</span>`<span style="white-space: pre-wrap;"> on button release.</span>

### Button mapping

<table id="bkmrk-buttondrive-modepick"><colgroup><col style="width: 160px;"></col><col style="width: 286px;"></col><col style="width: 356px;"></col></colgroup><tbody><tr><th>Button

</th><th>Drive mode

</th><th>Pickup mode

</th></tr><tr><td>Start

</td><td>Toggle pickup mode

</td><td>Toggle pickup mode

</td></tr><tr><td>Select

</td><td><span style="white-space: pre-wrap;">Toggle </span>`<span class="editor-theme-code">drive_manual_mode</span>`

</td><td><span style="white-space: pre-wrap;">Toggle </span>`<span class="editor-theme-code">arm_manual_mode</span>`

</td></tr><tr><td>Left trigger (LB)

</td><td>Toggle latching brake

</td><td>Ramp gripper speed → close (−1.0)

</td></tr><tr><td>Right trigger (RB)

</td><td>—

</td><td>Ramp gripper speed → open (+1.0)

</td></tr><tr><td>Left trigger 2 (LT)

</td><td>Toggle latching brake

</td><td>—

</td></tr><tr><td>Right trigger 2 (RT)

</td><td>Momentary brake (hold)

</td><td>—

</td></tr><tr><td>D-pad up

</td><td>—

</td><td>Ramp Z → up (+1.0)

</td></tr><tr><td>D-pad down

</td><td>—

</td><td>Ramp Z → down (−1.0)

</td></tr></tbody></table>

### Axis mapping

<table id="bkmrk-axisdrive-modepickup"><colgroup><col style="width: 117px;"></col><col style="width: 184px;"></col><col></col></colgroup><tbody><tr><th>Axis

</th><th>Drive mode

</th><th>Pickup mode

</th></tr><tr><td>Left stick Y

</td><td>Forward / backward

</td><td>Flick

</td></tr><tr><td>Left stick X

</td><td>Turn

</td><td>Rotate

</td></tr><tr><td>Right stick X

</td><td>—

</td><td>End effector X (left/right)

</td></tr><tr><td>Right stick Y

</td><td>—

</td><td>End effector Y (forward/backward)

</td></tr></tbody></table>

All analog axes pass through a deadzone (`<span class="editor-theme-code">±0.05</span>`<span style="white-space: pre-wrap;">) and are only dispatched when the change from the last-sent value exceeds </span>`<span class="editor-theme-code">AXIS_CHANGE_THRESHOLD</span>`<span style="white-space: pre-wrap;"> (0.05). Z and gripper speed are not axis-driven — they are ramped from button presses.</span>

### Packets sent

<table id="bkmrk-packetwhenbasestatio"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Packet

</th><th>When

</th></tr><tr><td>`<span class="editor-theme-code">BasestationManualDrive</span>`

</td><td>Drive mode, on axis change or heartbeat

</td></tr><tr><td>`<span class="editor-theme-code">BasestationManualBrake</span>`

</td><td>Drive mode, on brake toggle/press/release or heartbeat; also sent continuously (engaged) during pickup heartbeat

</td></tr><tr><td>`<span class="editor-theme-code">BasestationManualArmMovement</span>`

</td><td>Pickup mode, on any arm state change or heartbeat

</td></tr></tbody></table>

<span style="white-space: pre-wrap;">All values are scaled from </span>`<span class="editor-theme-code">[−1.0, 1.0]</span>`<span style="white-space: pre-wrap;"> to the full </span>`<span class="editor-theme-code">sint32</span>`<span style="white-space: pre-wrap;"> range before transmission.</span>

### Constants

<table id="bkmrk-constantvaluepurpose"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>Constant

</th><th>Value

</th><th>Purpose

</th></tr><tr><td>`<span class="editor-theme-code">AXIS_CHANGE_THRESHOLD</span>`

</td><td>`<span class="editor-theme-code">0.05</span>`

</td><td>Deadzone boundary and minimum delta before a packet is sent

</td></tr><tr><td>`<span class="editor-theme-code">HEARTBEAT_INTERVAL</span>`

</td><td>`<span class="editor-theme-code">2 s</span>`

</td><td>How often state is re-sent without an input event

</td></tr><tr><td>`<span class="editor-theme-code">RAMP_DURATION_SECS</span>`

</td><td>`<span class="editor-theme-code">1.0 s</span>`

</td><td><span style="white-space: pre-wrap;">Time for a ramped axis to travel from </span>`<span class="editor-theme-code">0.0</span>`<span style="white-space: pre-wrap;"> to </span>`<span class="editor-theme-code">±1.0</span>`

</td></tr><tr><td>`<span class="editor-theme-code">RAMP_TICK_MS</span>`

</td><td>`<span class="editor-theme-code">16 ms</span>`

</td><td>Ramp thread tick interval (~60 Hz)

</td></tr><tr><td>`<span class="editor-theme-code">MOMENTARY_BRAKE_DURATION</span>`

</td><td>`<span class="editor-theme-code">500 ms</span>`

</td><td>Defined but unused — auto-release was commented out

</td></tr></tbody></table>

# checks.rs — Diagnostics

Two utility commands for diagnostics and maintenance.

**`<strong class="editor-theme-bold editor-theme-code">ping()</strong>`**<span style="white-space: pre-wrap;"> Prints </span>`<span class="editor-theme-code">"PING FROM RUST"</span>`<span style="white-space: pre-wrap;"> to the console. Used to verify the Tauri bridge is working.</span>

**`<strong class="editor-theme-bold editor-theme-code">clear_cache()</strong>`**<span style="white-space: pre-wrap;"> Deletes all contents of the </span>`<span class="editor-theme-code">base_station/</span>`<span style="white-space: pre-wrap;"> folder inside the system cache directory. Exposed as a callable command so the frontend can trigger a manual cache wipe.</span>

`<span class="editor-theme-code">clear_cache_on_startup()</span>`<span style="white-space: pre-wrap;"> is the internal (non-command) version called automatically every time the app launches.</span>

<p class="callout danger">**Note for future developers:**<span style="white-space: pre-wrap;"> Clearing the cache on startup is necessary, without doing so the video feed and other assets will not load in some devices.</span></p>

# load_model.rs — 3D Model Loading

Handles loading of 3D model files bundled with the application.

**`<strong class="editor-theme-bold editor-theme-code">load_model(path) → Vec<u8></strong>`**<span style="white-space: pre-wrap;"> Reads a model file by filename and returns its raw bytes to the frontend. In debug builds, models are loaded from </span>`<span class="editor-theme-code">src-tauri/models/</span>`<span style="white-space: pre-wrap;">. In release builds, they are loaded from Tauri's resource directory (where they are bundled via </span>`<span class="editor-theme-code">tauri.conf.json</span>`).

**`<strong class="editor-theme-bold editor-theme-code">debug_resource_dir()</strong>`**<span style="white-space: pre-wrap;"> Returns the resource directory path and its contents as a string. Used during development to verify that model files are bundled correctly.</span>

<p class="callout info">**Note for future developers:** Webview technically do this automatically from the frontend but it errors out so the custom function is necessary.</p>

# rover_commands.rs — Rover Science Commands

<p class="callout warning">This file is work in progress</p>

These commands are called by the frontend to request measurements or send data to the rover. All four are currently partially stubbed — they simulate a 1-second rover response delay and return dummy values, but the actual UDP dispatch logic is ready to be wired in.

**`<strong class="editor-theme-bold editor-theme-code">request_coordinates() → (i16, i16)</strong>`**<span style="white-space: pre-wrap;"> Requests the rover's current GPS coordinates. Returns a </span>`<span class="editor-theme-code">(latitude, longitude)</span>`<span style="white-space: pre-wrap;"> tuple.</span>

**`<strong class="editor-theme-bold editor-theme-code">request_weight() → i16</strong>`**<span style="white-space: pre-wrap;"> Requests the weight of a collected rock sample from the rover's load cell. Returns a weight value in grams.</span>

**`<strong class="editor-theme-bold editor-theme-code">request_measurement(camera1, x1, y1, camera2, x2, y2) → i16</strong>`**<span style="white-space: pre-wrap;"> Requests a stereo distance measurement. The frontend passes two pixel coordinates (one per camera) and the rover computes the real-world distance to that point. Returns the distance in a unit TBD.</span>

**`<strong class="editor-theme-bold editor-theme-code">send_pixel(camera, x, y)</strong>`**<span style="white-space: pre-wrap;"> Sends a single pixel coordinate from the frontend to the rover (e.g. operator clicking on an object in the video feed). Used to direct the rover's attention or arm toward a specific point in the image.</span>

# map_commands.rs and map_processor.rs — 3D Map Rendering & Coordinate Transforms

**`<strong class="editor-theme-bold editor-theme-code">map_processor.rs</strong>` + `<strong class="editor-theme-bold editor-theme-code">map_commands.rs</strong>` — 3D Map Rendering &amp; Coordinate Transforms**

Converts 3D map files into top-down orthographic PNG images coloured by height (Z), and exposes coordinate transforms so the frontend can convert pixel clicks back to real-world metres. The pipeline runs on a blocking thread to avoid stalling the async runtime.

### Supported formats

<table id="bkmrk-extensionlibrarynote"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>Extension

</th><th>Library

</th><th>Notes

</th></tr><tr><td>`<span class="editor-theme-code">.obj</span>`

</td><td>`<span class="editor-theme-code">tobj</span>`

</td><td>OBJ Y-up convention remapped: world X = OBJ X, world Y = OBJ Z, height = OBJ Y

</td></tr><tr><td>`<span class="editor-theme-code">.las</span>`

</td><td>`<span class="editor-theme-code">las</span>`

</td><td>X/Y/Z read directly from point records

</td></tr><tr><td>`<span class="editor-theme-code">.laz</span>`

</td><td>`<span class="editor-theme-code">las</span>`

</td><td>Same as LAS (compressed)

</td></tr></tbody></table>

### Commands

**`<strong class="editor-theme-bold editor-theme-code">render_map(filename)</strong>`**<span style="white-space: pre-wrap;"> → </span>`<span class="editor-theme-code">MapMeta</span>`<span style="white-space: pre-wrap;"> Renders a 3D map file to a top-down PNG. The source file must already exist in </span>`<span class="editor-theme-code"><appDataDir>/maps/</span>`<span style="white-space: pre-wrap;">. The output is written to the same directory as </span>`<span class="editor-theme-code"><stem>_preview.png</span>`<span style="white-space: pre-wrap;">. The image is sized to a 2048 px longest edge, aspect ratio preserved. Heavy work is offloaded via </span>`<span class="editor-theme-code">spawn_blocking</span>`<span style="white-space: pre-wrap;"> so the async runtime is never blocked. Returns a </span>`<span class="editor-theme-code">MapMeta</span>`<span style="white-space: pre-wrap;"> struct the frontend uses for pixel→world transforms.</span>

**`<strong class="editor-theme-bold editor-theme-code">pixel_to_world(px, py, meta)</strong>`**<span style="white-space: pre-wrap;"> → </span>`<span class="editor-theme-code">(f64, f64)</span>`<span style="white-space: pre-wrap;"> Converts a 2D pixel coordinate (origin bottom-left, X right, Y up) to real-world metres. Expects coordinates in the displayed image's frame — rotation, if any, must be accounted for by the frontend before calling this. The formula is simply </span>`<span class="editor-theme-code">world = pixel × metres_per_pixel</span>`, with the world origin anchored to the bottom-left corner of the image.

### `<span class="editor-theme-code">MapMeta</span>`<span style="white-space: pre-wrap;"> fields</span>

`<span class="editor-theme-code">MapMeta</span>`<span style="white-space: pre-wrap;"> is serialised and returned to the frontend after every </span>`<span class="editor-theme-code">render_map</span>`<span style="white-space: pre-wrap;"> call.</span>

<table id="bkmrk-fieldtypedescription"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>Field

</th><th>Type

</th><th>Description

</th></tr><tr><td>`<span class="editor-theme-code">img_width</span>`

</td><td>`<span class="editor-theme-code">u32</span>`

</td><td>PNG width in pixels (post-rotation)

</td></tr><tr><td>`<span class="editor-theme-code">img_height</span>`

</td><td>`<span class="editor-theme-code">u32</span>`

</td><td>PNG height in pixels (post-rotation)

</td></tr><tr><td>`<span class="editor-theme-code">world_x_min</span>`

</td><td>`<span class="editor-theme-code">f64</span>`

</td><td><span style="white-space: pre-wrap;">Real-world X at the left edge (currently always </span>

`<span class="editor-theme-code">0.0</span>`

)

</td></tr><tr><td>`<span class="editor-theme-code">world_y_min</span>`

</td><td>`<span class="editor-theme-code">f64</span>`

</td><td><span style="white-space: pre-wrap;">Real-world Y at the bottom edge (currently always </span>

`<span class="editor-theme-code">0.0</span>`

)

</td></tr><tr><td>`<span class="editor-theme-code">metres_per_pixel</span>`

</td><td>`<span class="editor-theme-code">f64</span>`

</td><td>Scale factor for pixel→world conversion

</td></tr><tr><td>`<span class="editor-theme-code">format</span>`

</td><td>`<span class="editor-theme-code">String</span>`

</td><td>Source format detected (

`<span class="editor-theme-code">"obj"</span>`

<span style="white-space: pre-wrap;">, </span>

`<span class="editor-theme-code">"las"</span>`

<span style="white-space: pre-wrap;">, </span>

`<span class="editor-theme-code">"laz"</span>`

)

</td></tr><tr><td>`<span class="editor-theme-code">rotated</span>`

</td><td>`<span class="editor-theme-code">bool</span>`

</td><td>`<span class="editor-theme-code">true</span>`

<span style="white-space: pre-wrap;"> if the image was rotated 90° to landscape</span>

</td></tr></tbody></table>

### Rendering pipeline

`<span class="editor-theme-code">process_map()</span>`<span style="white-space: pre-wrap;"> runs the full pipeline in five stages:</span>

1. **Load**<span style="white-space: pre-wrap;"> — Parse the source file into a flat list of </span>`<span class="editor-theme-code">Point3D</span>`<span style="white-space: pre-wrap;"> structs (</span>`<span class="editor-theme-code">x</span>`<span style="white-space: pre-wrap;">, </span>`<span class="editor-theme-code">y</span>`<span style="white-space: pre-wrap;">, </span>`<span class="editor-theme-code">z</span>`<span style="white-space: pre-wrap;"> in world space).</span>
2. **Bounding box**<span style="white-space: pre-wrap;"> — Compute </span>`<span class="editor-theme-code">x_min/max</span>`<span style="white-space: pre-wrap;">, </span>`<span class="editor-theme-code">y_min/max</span>`<span style="white-space: pre-wrap;">, </span>`<span class="editor-theme-code">z_min/max</span>`<span style="white-space: pre-wrap;"> over all points. World width and height must be non-zero or an error is returned.</span>
3. **Rasterise**<span style="white-space: pre-wrap;"> — Map each point to a pixel coordinate. Where multiple points land on the same pixel, keep the highest Z (i.e. the sky-facing surface wins). Pixel dimensions are derived from </span>`<span class="editor-theme-code">img_size</span>`<span style="white-space: pre-wrap;"> (2048) with the aspect ratio preserved.</span>
4. **Gap fill**<span style="white-space: pre-wrap;"> — Run a two-pass nearest-neighbour distance transform (</span>`<span class="editor-theme-code">fill_gaps</span>`) to fill pixels that received no points. The forward pass sweeps top-left → bottom-right (checking left and top neighbours); the backward pass sweeps bottom-right → top-left (checking right and bottom neighbours). Each unfilled pixel inherits the Z of its closest filled neighbour, eliminating stripe artifacts.
5. **Colour + save**<span style="white-space: pre-wrap;"> — Each pixel's Z is normalised to </span>`<span class="editor-theme-code">[0.0, 1.0]</span>`<span style="white-space: pre-wrap;"> and passed through a five-stop colour ramp (</span>`<span class="editor-theme-code">height_color</span>`): deep blue → cyan → green → yellow → red. If all points share the same Z (flat terrain), mid-green is used. The image is rotated 90° if height exceeds width (to keep the longest edge horizontal), then saved as PNG.

### Height colour ramp

<table id="bkmrk-normalised-zcolour0."><colgroup><col></col><col></col></colgroup><tbody><tr><th>Normalised Z

</th><th>Colour

</th></tr><tr><td>0.0

</td><td>Deep blue

</td></tr><tr><td>0.25

</td><td>Cyan

</td></tr><tr><td>0.5

</td><td>Green

</td></tr><tr><td>0.75

</td><td>Yellow

</td></tr><tr><td>1.0

</td><td>Red

</td></tr></tbody></table>

### Error conditions

<table id="bkmrk-conditionerror-retur"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Condition

</th><th>Error returned

</th></tr><tr><td>Unsupported file extension

</td><td>`<span class="editor-theme-code">"Unsupported format: .{ext}"</span>`

</td></tr><tr><td>File parsed but empty

</td><td>`<span class="editor-theme-code">"File parsed but contained no points."</span>`

</td></tr><tr><td>All points collinear in X or Y

</td><td>`<span class="editor-theme-code">"All points are collinear — cannot build a 2D map."</span>`

</td></tr><tr><td>Source file missing at invoke time

</td><td>`<span class="editor-theme-code">"File not found: {path}"</span>`

</td></tr><tr><td>PNG write failure

</td><td>`<span class="editor-theme-code">"Failed to save PNG: {e}"</span>`

</td></tr></tbody></table>