flyvei

Notes from the v1 build

Flyvei is a desktop tool that turns a flight's GPS trace into 3D-printable terrain sculptures. Drop in an ADS-B trace, pick which leg to render, and a few minutes later you have terrain.stl and track.stl in an organized folder, ready to slice. Built to handle the production pipeline for a coming Skogsdue product line: physical commemorative pieces for general aviation pilots.

I built it solo over five days. These are notes on the decisions behind v1.

Flight detection: PH-HGB with two legs, advanced settings expanded.
Flight detection: PH-HGB with two legs, advanced settings expanded.

The shape of the problem

Pilots want a memento of a specific flight. Type II Studio does this for cycling and running, Drawscape does airport diagrams. Nothing serious exists for general aviation in 3D. The data is there, ADSBexchange logs every transponder, and SRTM has 30m elevation data globally. The sculpture itself is just a terrain relief with the flight path as a coral ridge that plugs in via small pegs. The hard part is going from raw flight data to a clean STL pair, fast enough that I can fulfill orders without losing a day per piece.

I wanted to type a tail number into the Chrome address bar, click an extension button, drag the resulting JSON into a desktop app, and have printable STLs out the other side. Five clicks. No Python scripts in a terminal, no debugging meshes by hand.

Pipeline first, app second

I built the pipeline as five Python CLI scripts before any UI existed. Parse GPX, fetch DEM, generate terrain mesh, generate track mesh, fix peg holes. Each stage saves intermediates to disk, each script runnable independently for debugging.

This was deliberate. I knew the geometry would be the hard part: getting a watertight terrain solid with peg holes that align to a 3D space curve, with the right vertical exaggeration so the Netherlands actually reads as terrain. Wrapping a half-working pipeline in a UI would have been a waste. Once the scripts produced a sculpture I'd hold and recognize as the right place, then I refactored.

The refactor lifted the actual logic into a flyvei/ Python package with typed function signatures. The CLI scripts became thin shells. The package is what the app imports.

Before: 23 files in one flat folder
Before: 23 files in one flat folder
After: per-flight directories.
After: per-flight directories.

Why Electron

Tauri would have been smaller and faster. Electron ships with its own Chromium, the app is 150MB before I write any code, and starts noticeably slower. Tauri uses the OS webview and ships in 5-15MB.

I picked Electron anyway. The backend work for a desktop app that runs Python and orchestrates files is meaningful: drag-and-drop handling, subprocess management, native file dialogs. In Electron that's JavaScript, the language I already use for keebloom's site and Batchlo's UI. In Tauri it's Rust, which I don't know.

The right answer for a one-person team is the stack you already speak. App size doesn't matter when I'm the user.

FastAPI as glue

The Python pipeline and the Electron app don't share a runtime. Two real options for talking between them: spawn Python as a subprocess and parse its stdout, or run Python as a local HTTP server and call it from the Electron app like any backend.

Subprocess is simpler. It's also fragile. Every script invocation has 1-2 seconds of Python startup cost, every error is text I have to parse, every change to a CLI breaks the contract.

Local FastAPI server is cleaner. Python runs once, exposes three endpoints, the Electron app calls them. Real exceptions, real progress callbacks, real types. Fifteen lines of FastAPI to wrap the existing library.

The server starts when the app launches and dies when it closes. The user never sees it.

Processing leg 1: stages, progress bar, live pipeline log.
Processing leg 1: stages, progress bar, live pipeline log.

Trace download as a Chrome extension

ADSBexchange doesn't have a download button. To get a trace JSON, the workflow is: open DevTools, filter network requests for trace_full, refresh the page, find the right request, right-click save as. Six steps to get one file.

I considered building the trace fetch into flyvei. Type a tail number, the app fetches the JSON. Two problems with that. First, ADSBexchange's URL pattern for historical traces requires a paid Patreon. Second, removing the manual step removes the verification: when I download a trace by hand, I'm also confirming it matches the customer's described flight before I process it.

The Chrome extension solves the actual friction. One toolbar button, the trace JSON downloads to my Downloads folder, I drag it into flyvei. Verification stays. ADSBexchange's API constraints are sidestepped because the extension uses the same recent-trace endpoint a logged-out browser does.

Thirty lines of JavaScript. Thirty seconds saved per order. Multiplies fast.

PH-HGB on globe.adsbexchange.com, extension notification confirming the trace was downloaded.
PH-HGB on globe.adsbexchange.com, extension notification confirming the trace was downloaded.

Things I cut

Water carving for v1. Recessing water bodies into the terrain and inlaying blue plastic was on the spec. The vector-to-raster pipeline I tried produced either fragmented rivers or 200+ unprintable specks I'd have to glue by hand. The right fix is a vector-first rebuild, polygons all the way through to the mesh. That's a day of focused work I'm deferring to v1.1.

Airport-anchored synthesis. The track currently extends backward from the first ADS-B point at a heading-and-descent-rate guess to reach ground level. For aircraft like older R44s without ADS-B Out, the first real point can be several hundred meters above terrain. The synthesized takeoff segment doesn't always land where the actual airport is. The fix is to take a departure ICAO as input and anchor the synthesis there, but it requires an airport coordinate database and a UI for the input. Deferred.

Flight history browser inside the app. Each processed flight gets a folder on disk with a deterministic name. If I want to find an old one, I open Explorer. Building a browser inside the app is feature creep for a tool with one user.

3D preview in the app. Bambu Studio is open on my second monitor anyway.

Terrain mesh, 65k faces, watertight, with carved peg holes.
Terrain mesh, 65k faces, watertight, with carved peg holes.
Track mesh, 60k faces, watertight, with pegs at start and end.
Track mesh, 60k faces, watertight, with pegs at start and end.

Smaller things

Folder naming. Each flight gets {registration}-{date}/, each leg gets leg-{n}-{dep_lat_lon}-to-{arr_lat_lon}/. The lat/lon coords are because airport ICAO detection isn't built yet. When it is, leg names get cleaner. Until then, they're at least unambiguous.

Edge fade. The terrain used to end in a hard vertical cliff at the bbox boundary. I added a 10% fade ramp that pulls elevation down to the base plate at the edges. The Veluwe ridge in the upper-right of NL flights now ramps off naturally instead of being knife-cut.

Layer lines as a feature. I print on a Bambu A1 at 0.2mm layer height, which produces visible horizontal layer lines on the terrain. They read as topographic contour lines. I'm not smoothing them out.

Peg tolerance. The first physical print had a too-tight peg fit on one end. PLA dimensional drift around 0.1-0.2mm makes the original 0.1mm/side clearance unreliable. Bumped hole diameter from 3.2mm to 3.4mm. Fits clean now.

What's next

The pipeline is robust for out-and-back two-leg flights. Loop flights and aircraft with poor ADS-B coverage at takeoff or landing reveal real bugs. Both go in v1.1.

Water carving is the biggest single visual upgrade I haven't shipped. Once it's in, the country shape pops and the sculpture works for any flight, not just those with strong terrain relief.

The thing I'm watching closest is whether the pipeline generalizes beyond Dutch flights. PH-HGB works perfectly. A Swiss aircraft in mountain terrain showed me edge cases I hadn't anticipated. Each new flight is a test.

v1.1 print: grey terrain, coral track, edge-to-edge with edge fade.
v1.1 print: grey terrain, coral track, edge-to-edge with edge fade.

Flyvei is internal, used to fulfill orders for the coming sculpture product. Questions: max@skogsdue.com.