FLIGHT viewer — the Facet pane, pedestrian WALK, and the ONE world frame
nornir:class=design
The FLIGHT half of Rickard's two-viewer partition (see osm-3d-gaps.md) finally has a host-facing pane, a street-level walk, and — the load-bearing part — a single, unambiguous world frame. This doc names only the chosen design.
1. FlightFacet — the pane that was missing
facett-map3d-flight shipped a complete engine (floating origin · atmospheric scattering · 6DOF rig · CDLOD terrain · day cycle · tile streaming) and no Facet. osm-3d-gaps.md row C:69 recorded it as PARTIAL for that reason. The consequence was not cosmetic: with no Facet there was no way for a FacetDeck to mount it, no way for a robot script to drive it, and no state_json/pixel oracle over it — so the engine's correctness was only ever checked by its own unit tests, in its own vocabulary.
facett-map3d-flight/src/facet.rs is that pane. The design:
- It owns no renderer. The pane holds FLIGHT-side state (the
FlightCamerarig, theDayCyclesun, the locomotion lane) and drivesfacett_map3d::Map3Das its shared render core. Extrusion, the watertight roof shell, ground-plane road ribbons and the CPU/wgpu painter are fixed once infacett-map3dand both viewers benefit — the partition doc's "shared render-core" rule, honoured literally (L5). - It duplicates nothing from the NAVIGATOR side. No turn-by-turn, no route overlay, no ETA/next-turn HUD. Those belong to the navigator and stay there.
- The camera bridge is a translation, not a second camera model. The rig's yaw/pitch are pushed into the render core through its existing Garmin vocabulary (
set_bearing_deg/set_tilt_deg), which is already the single source of truth both viewers read. - Gated on the
eguifeature. AFacetis by definition an egui pane; the engine core stays egui-free and headless-testable.
state_json publishes the whole pose — lon/lat/altitude_m/ground_m/ eye_height_m, yaw_deg/pitch_deg/roll_deg, locomotion, hour, sun_elevation_deg, cloudiness — with the render core's own state folded in under map, so the pixel oracle (RenderReport.vertices + non-zero geometry) applies unchanged and the state oracle is exact.
2. Locomotion::Walk — the pedestrian street-level mode
osm-3d-gaps.md row C:72 was the last open FLIGHT-owned camera gap: "no pedestrian eye-height locomotion yet — the tour drive is a car-nav chase, not a first-person walk", explicitly a FLIGHT feature and explicitly not a navigator one.
Locomotion is a superset of the engine's FlyMode — Fly / Stick / Orbit map onto it one-to-one, and Walk rides the free-fly integrator with three pedestrian constraints layered on top. Each constraint is a separate contract with its own RED-when-broken test:
| Constraint | Value | Why it is a real contract |
| Eye pinned above ground | WALK_EYE_HEIGHT_M = 1.70 |
Entering Walk snaps the eye; there is no frame in which a "pedestrian" is still at cruise altitude. set_ground_m (the host's DEM sample) moves the walker with the terrain. |
| Horizontal-only travel | vertical input dropped, forward flattened to the ground plane | Without it, looking up and pressing forward lifts you off the street — Walk would just be slow flying. walking_forward_while_looking_up_does_not_take_off walks 100 s at the neck limit and asserts zero altitude gain. |
| Walking pace | WALK_SPEED_MPS = 1.40 (≈ 5 km/h) |
A jet-speed request must still cover ≤ 1.40 m in a second. |
| Neck clamp | WALK_PITCH_LIMIT = 70° |
A human neck does not hinge to the vertical; unclamping it is what turns a walk back into a flight. |
In Walk the render core's dolly is pulled to the closest legal radius, so the walker stands inside the extruded city rather than above it.
3. THE WORLD FRAME — one frame, one swap (the bug this round found)
The sharp finding. WorldPos defines, and its own methods enforce, the frame
WorldPos = { x: Mercator easting, y: Mercator northing, z: altitude }
from_lonlat writes it, to_lonlat reads y as the northing, ground_distance measures (x, y), and terrain.rs states the render swap explicitly at the boundary: positions.push([r[0], r[2], r[1]]) // → Y-up render frame [easting, elev, northing].
Three places disagreed with that, all in the same direction — they treated slot 1 as altitude:
camera_fly::forward()returned[cos·cp, sin(pitch), sin·cp], putting the vertical component in the northing slot.right()crossed against[0,1,0].integrate_orbitbuilt its offset the same way. Net effect: climbing moved the aircraft north (andto_lonlat()reported a changed latitude), while turning changed its altitude.cinematic.rsdocumented the render layout inside aWorldPosand built its orbit that way — so an autopilot "altitude" was silently a northing and the floating-origin snap quantised the wrong axis.facett-demo'sosm_fly.rsandbin/flight_demo.rsstored the camera pre-swapped into a Y-upWorldPosto make it agree with (1).
Nothing caught it because every rig test was basis-relative: they compared motion against forward() itself, so a consistently-wrong basis passed all of them, and the crate had no Facet tying the rig back to the world frame. This is the same failure shape as the SCREEN_BOUND_K smear: a suite that measures a thing against itself.
The chosen design
origin.rs is the authority. There is one world frame, and exactly one swap, at the render boundary.
camera_fly::forward()is[cos(yaw)·cp, sin(yaw)·cp, sin(pitch)];right()crosses against world up[0,0,1];integrate_orbitmatches.cinematic.rsbuilds its orbit in the world frame (zis the lift axis).- The demo binaries keep the camera in the world frame and apply
[v0, v2, v1]once, to the camera basis, at the point where they already swap geometry.
The oracle that pins it
camera_fly::tests::rig_climbs_in_altitude_not_latitude is deliberately not basis-relative — it asks the world what happened:
- flying straight up gains > 99 m of
zand moves < 2 m over the ground (ground_distance), where the pre-fix basis gained ~0 m of altitude and ~100 m of northing; - level flight at yaw 0 increases the longitude and holds altitude exactly;
move_upis a pure 250 m climb that moves you nowhere on the map.
The pane-level twin is climbing_changes_the_published_altitude_not_the_latitude in tests/facet_robot.rs, which reads the same contract off state_json.
4. What this leaves open
- The
Walkmode has no collision: a walker passes through building walls. The render core carries no per-footprint identity yet (the picking gap, D:84), and collision wants the same footprint index. Do picking first, then collision. set_ground_mis host-pushed. Wiring it toterrain::elevation_atinside the pane would make the walk follow real DEM relief without host cooperation.- The pane drives
Map3D's orbit camera through bearing/tilt; a true 6DOF free-look (roll) is flattened, because the render core's camera is an orbit rig. A roll-capable core is afacett-map3dchange, not a flight one.