TaskFlight renders an AH-64D, a UH-60L, a CH-47F, an AH-6I and a Boeing 737-800, and not one of them came from a model marketplace. There is no glTF in the repository, no FBX, no downloaded asset of any kind. Every airframe is procedural: geometry constructed in code from primitives, lathes and extrusions, textured from a shared PBR material bank with per-airframe paint schemes. The five models carry 37, 30, 35, 25 and 27 selectable parts respectively — 154 components a student can click, isolate and open maintenance data for.
Why not just buy models?
- Provenance. The AH-64 is an ITAR-controlled article. A marketplace model of unknown derivation is a question we would rather not have to answer in a government procurement. A training representation authored from public reference imagery, in code we wrote, has a one-sentence provenance story.
- Licensing. No per-seat asset licences, no redistribution clauses to audit before an offline classroom deployment, nothing that complicates handing a customer a fully static build.
- Selectability is the product. A purchased mesh is one object; we need every inspection-relevant component to be an individually pickable, isolatable, nameable part. Retro-fitting that onto someone else’s topology is more work than authoring geometry that is born segmented.
- Diffs. Geometry in code is reviewable in a pull request. A binary asset is not.
The coupling is string ids — so it is validated
The 3D model and the maintenance database are coupled by nothing but string ids: a part mesh declares id="ah64_tail_rotor_gearbox" and the catalogue holds a record with the same id. Both failure modes are silent at runtime — an unmatched mesh is merely unclickable, an unmatched record merely unreachable — so the build runs a validator that cross-checks every id in both directions across all five airframes before Next.js starts. This validator has caught real defects, including an entire airframe’s catalogue that was authored but never imported, which no amount of clicking on the other aircraft would have surfaced.
The runway is procedural too — and measured
When the 737 joined the fleet it spawned on a 220 m helicopter ramp, which is not a thing you can take off from. The fix is a 2,500 m × 45 m runway with ICAO markings, a parallel taxiway with holding points and edge lighting — all procedural vertex-coloured geometry costing five draw calls. The length is measured, not guessed: the flight model reaches Vr at 1,053 m of roll and lifts off at 1,448 m, so 2,500 m carries the ~1,000 m margin a real narrow-body runway does. The correctness risk was the runway surface disagreeing with the terrain height function and floating or sinking the aircraft; both are built from the same constants, and 2,505 samples across the footprint deviate by 0.000 m — a 2 cm gear-to-terrain error across the entire takeoff roll.
Scale is data, not tuning
A fleet spanning a 1.4-tonne Little Bird and a 737 broke every camera number tuned to the Apache — framed with the trainer’s original presets, an airliner four times the Apache’s size sits mostly off-screen. Framing now derives from a per-aircraft orbit radius in the registry, with the Apache’s ratio landing at exactly 1.0 so the original airframe is provably unchanged. The same registry drives camera offsets in the simulator: adding a sixth aircraft means authoring its numbers, not re-tuning cameras.
Growing the fleet made the app lighter
Five airframes of maintenance content is 2.3 MB of source, and importing it statically put every airframe into the first load of both routes — a trainee works on one aircraft at a time and was paying to download four they never opened. The catalogue now reaches each airframe through a dynamic import, so each becomes its own chunk fetched on selection; concurrent callers share a single in-flight request, and a failed load stays retryable instead of leaving a permanently rejected promise behind.
- / (trainer) First Load JS
- 638 kB → 136 kB
- /fly (simulator) First Load JS
- 660 kB → 158 kB
- Original single-aircraft build
- 251 kB
- External 3D assets
- 0