--- name: coordinate-frames description: > Use when a rover, camera, terrain tile, trajectory, planet, or link jitters, moves in the wrong direction, changes altitude while stationary, loses its orientation after a view switch, or when adding a new orbital/body-fixed reference frame. Also use for BigSpace, CellCoord, FloatingOrigin, ActivePhysicsFrame, or frame-conversion work. --- # Coordinate frames and BigSpace Use this runbook for coordinate changes. The concise design contract is [`docs/architecture/45-big-space-correct-usage.md`](../../docs/architecture/45-big-space-correct-usage.md). ## Find the semantic owner first Every astronomical or surface pose has a semantic `ReferenceFrame`: - `World` for the persistent scene frame; - `EclipticJ2000 { center }` for non-rotating body-centred work; - `BodyFixed { body }` for a rotating surface frame. Resolve it through `ReferenceFrameIndex`. Never select the first `Grid`, walk an arbitrary parent, or add a second frame marker to a precision sub-grid. Missing and duplicate declarations must remain errors (`None`). ## Use the existing conversion path 1. Read the authoritative f64 pose from USD, ephemeris, Modelica, or Avian. 2. Convert source → target semantic frame with the existing f64 frame helpers. 3. Resolve the target `Grid` from `ReferenceFrameIndex`. 4. Split once with `Grid::translation_to_grid`. 5. Attach/migrate atomically with `lunco_core::attach::migrate_to_grid`. `CellCoord`, `Transform`, and `GlobalTransform` are private projection state. They never cross a user/API/network/model boundary and never become the authoritative source of an astronomical or physics value. For immutable high-precision presentation entities, use BigSpace's existing `Stationary` marker. Streamed globe/terrain visual tiles may use it because their placement is fixed until the entity is replaced; never use it on a camera, avatar, physics tile, or any entity that can mutate `CellCoord`, `Transform`, or `ChildOf`. Remove `Stationary` before relocating an entity, as required by BigSpace. Application schedule gates may skip BigSpace work only from authoritative spatial invalidations. `LocalFloatingOrigin::is_local_origin_unchanged` reports the result of its most recent computation; after an origin changes, admit one follow-up local-origin computation to settle that flag. Do not treat a retained `false` as a permanent high-precision invalidation. Verify an origin shift, its settle pass, then stable frames with propagation closed. Track the pending settle between admitted passes rather than scanning every grid in an idle gate. For a camera, compose the selected camera's authoritative f64 pose into the persistent `WorldGrid` and update the grid-direct `OriginAnchor`'s `(CellCoord, Transform)` split. `OriginAnchor` is the sole owner of `FloatingOrigin`; cameras never receive or transfer that marker. For a site-anchored scene, celestial placement mounts only the authored site root. The root becomes the nested site `Grid` and `ActivePhysicsFrame` as soon as its `SiteAnchor` is projected, then is atomically migrated beneath the matching body-fixed surface Grid when that hierarchy is ready without changing the active frame. Terrain and rover/lander roots are sibling top-level children of that scene Grid; a rover is never parented to terrain. Each such top-level prim carries its own `CellCoord`, while visual and collision descendants remain ordinary children rooted in `LowPrecisionRoot`. Celestial placement does not query or migrate an avatar/camera. If the site Grid is reparented, the physics bridge reseeds bodies from the new site-local hierarchy and rotates only their velocity vectors; a frame switch without reparenting transports the existing physics pose. The avatar subsystem captures a loader-relative local-camera pose after USD projection has committed and applies it at the explicit scene-handoff boundary. All explicit camera frame changes stay with the camera subsystem through the same atomic migration helper. For a physical entity, keep it under `ActivePhysicsFrame` and let `BigSpacePhysicsBridgePlugin` own the Avian f64 pose exchange. The shared `lunco-physics::avian_backend` contract owns numeric backend admission; the bridge owns lifecycle admission and raises the runtime fault when changed poses or collider AABBs are invalid. It gates the nested physics phases before Avian grows AABBs or runs a query; `GridSpatialQuery` reuses the same point check after conversion. Keep the evidence layers separate: malformed composed USD transforms fail during projection, mutable runtime pose/AABB failures raise `RuntimeFaults` plus `PhysicsHolds::SAFETY_FAILURE`, and invalid public query origins return no hit without becoming a simulation fault. Use the existing bridge and scene-teardown tests for the first two, and a production Rhai scene test for the public query contract; do not add a second per-producer filter. For a line joining two frames, convert both endpoints into one semantic frame before generating cell-local geometry. For authored motion directly beneath a BigSpace `Grid`, use standard USD `double3 xformOp:translate` samples and split the f64 position with `Grid::translation_to_grid` before writing the local `Transform`. Unbound samples use `SimulationPresentationTime`, which stays between completed physical ticks and holds while transport is paused. Do not introduce a mission-specific trajectory component or independent clock for motion already represented by USD animation. Celestial body ephemerides are not ordinary USD animation: body frames, rotation, the semantic SunState, shadows, and the sky readout share one `lunco_time::CelestialTime` sample. It is a child of `WorldTime` and can be rate-scaled up to 100,000× without changing Avian's fixed physics cadence. Modelica reads celestial-derived environment inputs at its ordinary communication points. From a lunar surface, Earth stays near one sky position because the Moon is tidally locked; expect libration, while Earth's single body-fixed grid continues to rotate for the day/night cycle. At 100,000×, a lunar month takes about 24 seconds. Test both the parent-relative `CellCoord`/`Transform` and the BigSpace-propagated `GlobalTransform`; local transform checks alone do not prove a rendered pose reached its consumer. The celestial cadence commits the `CelestialTime` sample it gated, and advances body position and spin together. For the local kinematic avatar, use the existing Avian `MoveAndSlide` query in `ActivePhysicsFrame`: convert the source Grid pose, displacement, and up vector with `grid_transform_between_grids`, perform one shape move, and convert the solved pose back before `Grid::translation_to_grid`. The avatar is a camera embodiment, so do not invent a USD body schema or read `GlobalTransform` as its collision authority. This path preserves the fixed solver/substep contract. For orbital camera views, keep presentation state on the avatar in `OrbitViewHistory`, keyed by the stable celestial ephemeris id. Capture a user-controlled `OrbitCamera` pose before switching targets or leaving orbit, and restore it only for that same body, including a later surface-to-orbit scroll entry. When no saved pose exists, derive the arrival direction from the camera's current radial region after resolving the target's inertial BigSpace grid. Do not use a fixed world-axis/Sun-facing arrival, a scene-wide pose cache, or a second transform writer. Clear this transient history with active-Twin teardown and avatar demotion. When an orbital view must frame a mission site, publish the scene's valid `SiteAnchor` position transformed into the body's inertial grid as a typed f64 vector. This location belongs to the scene frame and does not depend on a possessed vehicle. Show the mission-site action as its own HUI card, apart from the mode selector. Rhai computes the orbit angles, reads the persisted `CameraInputSettings.orbit_direction_animation_duration_s` property, and sends the generic `AnimateOrbitCameraDirection` command. The camera transition moves along the current orbit without changing its radius or vertical offset; the orbit writer commits each BigSpace pose and refreshes the orbital pin. Direct user look input cancels the transition. Surface/orbit transitions use the persisted `CameraInputSettings.surface_mode_engage_altitude_m` property as the shared engage altitude and orbital zoom floor (1,000 m by default). Clearance uses sampled local DEM terrain where the active terrain covers the camera and the body's reference radius outside that coverage. Its companion `surface_mode_disengage_altitude_m` property provides hysteresis and defaults to 2,000 m under the same rule. For transform gizmos, use `transform-gizmo-bevy` only as a render-space frontend on an unparented proxy. Capture through `SimulationPoseQuery`, keep the proposed pose in the explicit `ActivePhysicsFrame`, convert the complete pose back with the canonical render/grid and parent-local helpers, and commit through one `TransformEntity` scene command. Never apply render deltas to a parent-local `Transform`, read `GlobalTransform` as physics authority, or write Avian `Position`/`Rotation` from editor code. Reproject from the active-frame transaction pose after BigSpace origin/cell changes. Because the frontend writes its final proxy pose in `Last` after the normal interaction transfer in `PostUpdate`, snapshot that final pose in `Last` before release cleanup. USD preview scale is authored through `UsdOp::SetScale`; live scale remains outside the physics contract. For USD geometry, `xformOpOrder` is the authoritative ordered transform stack. Read the complete composed local transform through the shared USD transform decoder, including scale; do not inspect individual `xformOp:*` attributes in a second path. For a new top-level scene or runtime spawn, convert the semantic f64 pose into the scene root's parent-local representation with `pose_in_grid_to_parent_storage`. The direct Grid child receives the returned `CellCoord` and local `Transform`; do not attach the rover under a terrain mesh or write a large parent-local f32 value with a zero cell. For Rhai scene tools, use the shared pointer coordinate contract. Rust converts the picking backend's `RenderPos` exactly once through the admitted `ActivePhysicsFrame`; the context exposes `world_position` as a tagged `point3` in `active_physics` and `render_position` as a tagged `render` point. Use the prelude helpers `point3`, `world_point`, `point_values`, `point_frame`, `point_delta`, `point_distance`, `point_offset`, and `pointer_point`. Never author from the render point, subtract raw arrays from points in another frame, or silently use the persistent world grid when the active nested site frame is unavailable. `pointer_point` is the user-facing failure boundary: it returns an explicit error for a missing hit or frame rather than an identity/floating-origin fallback. This keeps all screen tools—terrain, route, gizmo, and future tools—on one conversion owner. Scene-tool behavior must consume the generic `context.pointer_intents`/`pointer_intent(context, name)` surface from the shared input settings; do not encode Alt, Shift, Ctrl, or mouse-button meaning inside a route or terrain tool. This keeps remapping a settings change while the coordinate conversion remains owned by the Rust frame adapter. For waypoint labels, author `lunco:billboard*` on the waypoint and let the generic billboard renderer consume its propagated `GlobalTransform`. The renderer uses the shared BigSpace world-pose machinery for the camera/subject range check; route projection does not add another distance or coordinate conversion owner. The terrain-grid/BigSpace hierarchy and the existing billboard path already own that conversion. The shared overlay wraps labels to a bounded width, clamps their backdrop inside the active viewport, and gives nearer labels first choice of non-overlapping camera-facing slots. A label with no safe slot is omitted for that frame rather than covering another marker. Editor-created waypoints use the canonical USD billboard authoring helper; runtime-only waypoints attach the same `UsdBillboard` data plus the generic `BillboardIndex` fact to the shared marker root. Keep both paths on this one renderer; do not overwrite `Name` or add a waypoint-specific overlay. For physics and co-simulation, resolve a demanded direction target from its composed position into each `EnvironmentProbe` frame through the shared BigSpace f64 helpers. Normalize the displacement once into `UnitDirection3`; frame rotations preserve that unit vector. The USD wire selects the source id, while the consumer model uses a generic target-direction input. `SunState` contains solar irradiance only. A static `DistantLight` contributes a framed ray through the same resolver only after composed-root celestial-source classification confirms that no finite celestial target owns `sun`. Celestial roots use their finite body target exclusively, and a prior static ray is withdrawn before probe resolution. Do not add per-body conversion systems, another coordinate cache, or a local solar clock. Invalid input bindings may withhold the optional observer camera, but cannot block creation of celestial targets used by the direction resolver. Publish `SunRenderState` from the finalized scene-sun `GlobalTransform` after `BigSpaceSystems::PropagateLowPrecision`. Any conversion of that render direction through a terrain `GlobalTransform` belongs after that phase in `PostUpdate`: static material wiring, horizon-cache validity/bake decisions, and streamed-tile shadow intent binding all consume that finalized frame. Put streamed-tile binding in the public `TerrainSurfaceSet::RenderShadowBinding` phase so it cannot observe a previous-frame terrain transform. Do not repair a stale projection with an offset or another per-frame transform writer. The projection is change-gated by the selected source revision and changed BigSpace ancestor chains, so stable frames do not rebuild f64 poses. The render backend samples the resulting cascades with Bevy's hardware 2x2 comparison filter in standard and high profiles. Keep that choice separate from the semantic sun angle and the authored cascade/range/bias policy; do not introduce a Gaussian/PCSS blur or tune physical light state to hide a filtering artifact. ## Do not patch symptoms Do not add a per-frame position correction, a fallback frame, a guessed parent, an epoch-specific offset, a raw f32 absolute position, or a second transform writer. Those hide the ownership error and will reappear at a grid boundary or view transition. ## Required tests Add the smallest real regression at the owning boundary: - frame index rejects missing and duplicate semantic grids; - f64 pose conversion round-trips position and rotation; - atomic migration preserves the pose across a cell boundary; - the Avian bridge is invariant to BigSpace re-splitting and celestial-parent rotation; - surface ↔ inertial camera transfer preserves target pose and up direction; - per-avatar orbital history restores independent body poses and clears with active-Twin teardown; - the selected camera projects through the persistent `WorldGrid` into the sole `OriginAnchor`, while duplicate or missing world-shell entities fail closed. - a standard render camera receives the hardware 2x2 shadow filter exactly once; fast mode remains unlit and does not attach it. Run focused checks first: ```sh scripts/run_rust_tests.sh -p lunco-core --lib -j 4 scripts/run_rust_tests.sh -p lunco-celestial -j 4 scripts/run_rust_tests.sh -p lunco-usd-avian -j 4 RUSTC_WRAPPER= cargo build -p lunco-luncosim --bin luncosim -j 4 ``` For visual acceptance, launch the built production binary head-full with an explicit free API port, inspect surface and inertial views, then send the API `Exit` command and verify the process and port are gone. Use `--no-ui` only for headless deterministic checks.