Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

.nornir/flight-facet-and-world-frame-design.md

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:

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:

  1. 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_orbit built its offset the same way. Net effect: climbing moved the aircraft north (and to_lonlat() reported a changed latitude), while turning changed its altitude.
  2. cinematic.rs documented the render layout inside a WorldPos and built its orbit that way — so an autopilot "altitude" was silently a northing and the floating-origin snap quantised the wrong axis.
  3. facett-demo's osm_fly.rs and bin/flight_demo.rs stored the camera pre-swapped into a Y-up WorldPos to 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.

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:

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