# 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>