---
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.
