Article · · 6 min read
From OpenStreetMap to a 3D print: where the geometry breaks
The five failure classes I track on every export of my map generator, from road masks with holes to fragments too thin for a nozzle, and the rules that keep a city model printable.
monadruk.com sells printable 3D models of real places. The customer picks an area on the map, and the generator turns OpenStreetMap and elevation data into a file a 3D printer accepts. Getting the data is easy. Getting geometry that survives a slicer is where nearly all the work goes.
The pipeline in one paragraph
Buildings, roads, water and parks come from OpenStreetMap as 2D shapes and are cleaned up with shapely. Elevation tiles become the ground. Footprints are extruded to their heights and merged with the terrain into one mesh; the district on my site is an 80 × 80 mm square with 800,438 triangles, printed in 74 layers of 0.2 mm. Every export then passes a set of checks.
The five things that break
- Road mask topology. Roads are buffered lines, and their union is full of small internal holes. A void inside the road network that is not backed by a building footprint is a defect and has to be collapsed.
- Fit between roads, parks and water. Each layer is cut from the others, and if the clearances are not explicit you get overlaps or gaps that a printer shows instantly.
- Buildings not seated on their base. On sloped terrain a footprint at one height floats above the ground on one side and sinks into it on the other.
- Unstable booleans. Boolean operations on large multipolygons are slow and fail on near-degenerate input; clipping masks to the bounds of the shape before cutting fixes most of it.
- Non-printable thin fragments. Slivers left between features are thinner than a nozzle can lay down and must be removed before export.
Rules that stopped the regressions
The most useful rules were about discipline, not algorithms. Change one geometry layer at a time and verify it with a control render. Do not apply broad smoothing, chamfer or buffer changes across roads, parks and grooves in one step. Keep fit rules explicit per side, in one place, instead of spreading them across processors.
# simplified: cut roads out of a park, then drop slivers a nozzle cannot print
park = park.difference(road_mask.intersection(park.envelope.buffer(5)))
parts = [p for p in explode(park) if min_width(p) >= nozzle_width]
park = unary_union(parts)Why a checklist beats a fix
Every new city brings new edge cases in OSM data. Instead of fixing them one by one, I track the five failure classes above as a checklist on every export. A new city either passes all five or tells me exactly which layer to look at.