---
name: author-usd-physics
description: >
  Author or diagnose LunCoSim USD physics: rigid bodies, colliders, joints,
  joint frames, drives, gravity, collision filtering, and scene teardown. Use
  for exploding vehicles, wrong hinge or slider motion, detached parts, bad
  contact, or scene-owned physics state. The critical contracts are two joint
  frames, explicit body attachment, standard UsdPhysics fields, and teardown
  for every scene-owned write. Use build-vehicle for mobility assembly.
---

# Authoring physics in USD

Physics is authored in USD and projected onto avian. USD is the source of
truth; the ECS is the projection. Use maintained `UsdPhysics` schemas for
topology, limits, and commandable drives. When USD has no material schema for a
physical concept, use the narrow LunCo applied API that owns that concept. Do
not add a LunCo schema when standard USD already owns the fact.

For a physical landing member, apply `PhysicsDriveAPI:linear` to a
`PhysicsPrismaticJoint`. Author the drive's `stiffness`, `damping`, `maxForce`,
and explicit force type; `targetPosition` and `targetVelocity` retain the
standard USD defaults unless the asset needs to state them explicitly. The
native Avian joint is the sole axial mechanism in the existing substep
schedule. Do not duplicate standard fields under `lunco:*`. Missing or invalid
fields fail projection; they are never replaced by a target, force cap, or
solver-resolution workaround.

Native joint `angle` and `displacement` commands preserve the configured drive
model and force/torque limit. They enable the motor and change its position
target with zero target velocity; author drive gains and limits through the
standard USD drive owner rather than through command-side tuning.

Omitted joint limits retain standard unbounded travel. The native port adapter
maps lower negative infinity and upper positive infinity to absent bounds;
malformed nonfinite limits remain invalid. Do not add artificial finite travel
limits to make a valid unbounded joint accept a command.

Startup joint topology is prepared while the shared fixed clock is held. The
BigSpace physics bridge seeds authored body poses in `PreUpdate`; USD joints
are projected in `JointPreparation` during `Update`, after USD simulation
projection and before the generic `JointAdmission` owner. This phase must not
wait for a first physics step to resolve bodies or seat a joint.

Initial body admission validates raycast-wheel support footprints against the
composed Avian collider geometry and its hit normal. It does not require a
`LocalGravity` sample from a fixed cycle that has not been admitted yet. Keep
authored-pose validation in lifecycle preparation; the first synchronized
physics cycle begins only after the complete admission set is ready.

For asset-level checks, the authored `assembly_audit` tool can inspect explicit
composed joint body relationships, cardinal axes, optional local frames,
rigid-body/joint coverage, and mass/inertia/collider manifests. Use those
reports to diagnose authored topology before changing solver settings. Pass the
exact Editor document id first to each stage-reading helper; `()` explicitly
selects the mounted live scene, not the focused preview. The
tool does not infer a body from a name, turn a raycast wheel into a rigid body,
or replace the standard USD physics owner.
For a single explicit decision per maintained part, use
`assembly_audit::physicality_report(doc, manifest)`. Entries choose
`physical` or `visual-only`; physical entries reuse the existing body/joint and
collider/mass fields, while visual-only entries require a non-empty reason and
must have no composed collision envelope. Unknown roles, duplicate paths,
missing prims, and incomplete coverage are errors. This is Rhai policy and
orchestration; it does not add a second collider reader or Rust asset rule.

## Live joint detachment is one lifecycle

`DetachJoint` is the generic entity-detach command. It is deliberately not
limited to entities carrying `PhysicsJointLink`: an ordinary non-joint target
uses the normal entity-removal path. A target carrying native or pending joint
state is classified as a joint and must use the physics lifecycle marker. The
command records any authored endpoint invalidation and never directly removes a
native joint. The `lunco-usd-avian-joints::JointAttachPlugin` consumes that marker at its Update
boundary and performs one transaction in this order:

1. retire the Avian `JointGraph` edge and its `PhysicsIslands` entry;
2. remove the native or pending typed joint and `JointComponentId`;
3. release the transient joint collision filter;
4. despawn the now-disposable joint entity.

The retirement owner only runs when a `PhysicsJointDetachRequested` marker is
present. Idle updates skip its exclusive graph transaction and world query.
Joint admission readiness is also gated by one combined query over the existing
`PendingUsdJoint` and `PendingJointAdmission` markers. The owner rechecks the
scene projection and endpoint readiness before each pending batch is admitted,
while settled updates with no pending joint skip that scan.

Scene teardown uses the same graph-retirement helper. This shared owner is what
prevents a component-removal observer and a recursive despawn from unlinking one
island edge twice. A command or Twin script must never implement a second joint
detach path, disable `JointDisabled` as a substitute, or infer joint completion
from the command acknowledgement; verify that the joint entity is gone and both
endpoint bodies continue finite stepping. If a target has joint state but no
`PhysicsJointLink`, the command warns and rejects it; the authored topology
linter is the repair/diagnostic path. Persistent intent journals the typed USD
active-state/remove operation separately, while `Interactive` remains a
throwaway live transition.

When an Editor workflow creates a new moving part, use
`assembly_edit::rigid_body_plan` for the explicit body schema and mass facts,
then add the shape, collider, and local transform in the same proposal. Use
`assembly_edit::revolute_joint_plan` for a rotating part: pass both body paths,
both authored frame anchors and quaternions, the cardinal joint axis, ordered
degree limits, and the collision policy. These helpers return typed operations
for review; they do not infer bodies from hierarchy or drive a mechanism with
Euler poses.
Use `assembly_edit::fixed_joint_plan` for a rigid adapter: it requires the
same explicit two body paths, two local anchors, two local quaternions, and
collision policy, and returns a standard `PhysicsFixedJoint` plan without
mutating the document.

For higher-level construction, use the dynamically reloadable
`assembly_builder` Rhai library. Its `frame_plan`, `cube_plan`,
`cylinder_shape_plan`, `movable_cube_plan`,
`component_bundle_facts`/`component_bundle_plan`,
`existing_rigid_body_plan`, and `hinge_plan` compose the same explicit standard
USD facts for new or referenced parts; it does not promote a completed body or
reuse an occupied standard joint identity, while its placement and cube
edge-alignment plans reject ambiguous parent/frame assumptions before the
proposal reaches the USD owner. Keep the generated plan in one reviewed
change set. A component bundle may author `PhysicsMassAPI` on its root and
collision APIs on its geometry, but it does not infer a rigid body or joint;
use the explicit body/joint/mount planners when the part is movable. A
successful Rhai plan is not permission to omit the frame, collider, mass, or
generation contract.

For an existing generic component whose geometry, frames, and mass must change
together, use `component_editor::update_plan` with an explicit bundle recipe.
Its `component_editor::update_context`/`selected_update_context` helpers are
read-only discovery; the returned plan still goes through the normal
proposal/review/commit path. The planner preserves topology and material
ownership and rejects role drift, so it is not a replacement for explicit body
or joint authoring.

For physical placement, use `assembly_builder::place_with_clearance_plan` with
explicit Cube envelopes, or
`assembly_builder::place_with_collision_clearance_plan` with exact moving and
blocker body paths for referenced, Cylinder, Mesh, or compound bodies. The
general tool reads aggregate `QueryUsdPrim { collision_bounds: true }` results
from the shared collision owner and requires a translation-only common parent
chain. Both paths reject overlap, insufficient gap, duplicate blockers, and
ambiguous frame/geometry data before proposal. Do not rely on visual
separation or an unreviewed generated USD transform to establish clearance.
For deterministic mating of non-Cube bodies, use the companion
`align_collision_centers_plan` or `align_collision_edges_plan` with exact
paths and an explicit axis/edge/gap; these plans preserve authored physics
facts and still require review before commit.

For reusable referenced parts, use `assembly_builder::find_compatible_socket`
and `assembly_builder::mount_component`. These helpers select authored socket
relationships and derive the reflected fixed/revolute/prismatic joint; the
existing `AttachComponent` owner remains responsible for plug-frame math,
reference lowering, occupancy, and atomic journalling. Do not copy a reference
and guess a transform when the component already advertises a mount plug.

For a part that is already mounted, use
`assembly_builder::mount_frame_realignment_plan` with the exact host, socket,
part, and recorded-joint paths. It is a dynamic Rhai plan for nested canonical
rigid frame composition and emits the part transform plus both exact joint
anchors as one reviewed operation set. It preserves topology and fails before
proposal on missing/ambiguous relationships, wrong plug/body identity,
unsupported frame stacks, malformed values, or non-unit scale.

When a model is a reusable referenced assembly rather than a socket attachment,
use `assembly_builder::referenced_instance_plan` or
`assembly_builder::referenced_instance_targeted_plan` when the source prim must
be explicit. Then wait for its composed children before using
`assembly_builder::select_variants_plan`. This keeps first-use asset loading
separate from the variant recompose and makes missing parents, occupied
identities, and invalid targets fail in Rhai before review. Parameter edits use
`assembly_builder::parameter_plan`; the resulting typed `SetAttribute` ops go
through the same proposal/journal boundary as every other authored edit.

Mission-level recipes may compose these generic helpers through a Twin-local
tool library. Keep those recipes and their study values in the owning Twin,
not in the core asset library, and keep their output to explicit generic USD
operations. The core should provide only reusable component facts, topology,
placement, and typed-op validation.

## Collision geometry and derived NURBS proxies

`QueryUsdPrim { collision_geometry: true }` reads the composed collider through
the same USD-to-Avian geometry reader used by projection. Use it when a
requirement must compare the backend shape to its visual or design geometry;
authored `radius`/`height`, `geometry_bounds`, and `collision_bounds` answer
different questions. This query predicts the geometry Avian will receive from
the current composed USD. It does not prove that a live ECS entity has completed
projection or is currently contacting the world.

For `UsdGeomNurbsPatch`, use the read-only `PlanNurbsCollisionProxy` query and
the shared `nurbs.rhai` proposal. Pass `max_refinement_delta_m` in canonical
metres. This bounds the sampled change between two successive tessellation
levels; it is a deterministic convergence criterion, not a certified maximum
distance from the exact rational surface. Review the proposal that creates an
invisible, source-linked `UsdGeomMesh` carrying standard mesh-collision APIs.
Projection re-cooks the source and rejects stale or edited derived geometry.
Do not author a separate collider approximation by hand or treat this threshold
as a CAD accuracy guarantee.

The Avian mesh adapter advertises its implemented USD approximation modes via
`AvianMeshCollisionApproximations`: `none`, `convexHull`, and
`convexDecomposition`. `none` is valid only for static or kinematic bodies.
Other standard USD tokens remain valid authored values, but this adapter
rejects them until it can realize their stated semantics; it does not silently
substitute another shape.

The local avatar is a runtime kinematic camera embodiment, not an authored
rigid body. Its `MoveAndSlide` capsule reuses the standard `UsdPhysics`
colliders projected by the Avian bridge in the active BigSpace frame. Do not
add an avatar collider schema or a second collision representation; see the
[BigSpace physics boundary](../../docs/architecture/45-big-space-correct-usage.md#physics-boundary)
for the frame conversion and Twin-scoped traversal policy.

## 1. A joint is TWO FRAMES, not an axis

This is the single most expensive thing to get wrong, and it fails silently.

A `UsdPhysicsJoint` is defined by a frame on **each** body:

| USD | avian | meaning |
|---|---|---|
| `physics:localPos0` / `localPos1` | `JointFrame::anchor` | where the joint attaches, in each body's local space |
| `physics:localRot0` / `localRot1` | `JointFrame::basis` | how the joint frame is ORIENTED, in each body's local space |
| `physics:axis` | `slider_axis` / `hinge_axis` / `twist_axis` | a CARDINAL axis **of the joint frame** — X, Y or Z is the whole vocabulary |

`physics:axis` can only name a cardinal axis. That is exactly why `localRot`
exists: it is how a mechanism that is not axis-aligned — a landing leg raked 25°
off vertical — says where its axis really points.

**Both halves must cross into the engine.** Every avian joint except the
spherical constrains relative ORIENTATION through `basis1`/`basis2`. An identity
basis therefore demands its body sit square to the other body. Carrying the rake
in the axis alone aims the slider correctly and still wrenches the strut 25° out
of true — the constraint is violated from the first step, the solver resolves it
impulsively through the strut's lever arm, and the vehicle disassembles at
kilometres per second.

**The rule when a body rests at an angle:** if body1 is rotated relative to
body0 in the authored rest pose, that rotation lives in the FRAMES. The joint
holds `rot0 · localRot0 == rot1 · localRot1`, so author whichever side is needed
to make that identity true at rest.

```usda
# LegPX is raked +25° about Z (its own xformOp:rotateXYZ).
def PhysicsPrismaticJoint "LegPX_Spring" (
    prepend apiSchemas = ["PhysicsDriveAPI:linear"]
)
{
    rel physics:body0 = </DescentLander>
    rel physics:body1 = </DescentLander/LegPX>
    uniform token physics:axis = "Y"          # cardinal, IN the joint frame
    quatf physics:localRot0 = (-0.216440, 0, 0, 0.976296)   # 205° about Z: +Y down the strut
    quatf physics:localRot1 = (0, 0, 0, 1)                  # the same 180° flip; the leg body
                                                            # already carries the 25°
}
```

Quaternions in USD are `(w, x, y, z)`. Angles — `physics:lowerLimit`,
`upperLimit`, `coneAngle0Limit`, `coneAngle1Limit` — are **degrees**.

### The diagnostic

The loader measures every joint against its authored frames at build and reports
a violation, seating the body only where the constraint determines it uniquely:

```
[usd-avian] joint /…/PadNX_Weld starts violated by 0.000 m / 0.436 rad — seating
`/…/PadNX` onto the authored joint frame. frame0: localPos0=… localRot0=…,
frame1: localPos1=… localRot1=…, body0 at …, body1 at …
```

`0.436 rad` is 25°. **An angular violation on a raked mechanism is almost always
a missing `physics:localRot1`**: body0's frame was authored off-cardinal and
body1's was left at identity, so the joint demands body1 sit square to body0.

Position is checked for every joint type. Orientation is checked and seated only
where all three rotational DOF are locked (fixed, prismatic); a revolute or
spherical joint leaves rotation free by design, so it is reported and left to
the solver.

## 1b. A rotating assembly is joints plus a controller — never Euler animation

An antenna, solar panel, camera gimbal, arm, or other tracking head has two
separate concerns:

1. **Mechanism:** each moving frame applies `PhysicsRigidBodyAPI` and is joined
   to its immediate predecessor with a standard `PhysicsRevoluteJoint`.
2. **Setpoint law:** a Modelica program computes radians and wires them to the
   joint's native `inputs:angle` port. The cosim joint backend owns the position
   motor only while that port is wired.

**The assembly owns its own hinges.** Compose the assembly's root directly onto
the host body and keep its internal joints and controller connections relative
to that root. The host supplies no duplicate attachment joint; it only chooses
to compose the assembly. Keep the visible mechanism under a unique child path.

This is the direction of dependency: a higher-level rover, lander, tower, or
ground station knows it installs an antenna; the antenna knows only its own
frames, joints, ports, and Modelica controller. The lower-level assembly never
names `Rover`, `Lander`, or any other concrete host.

**Never drive a tracking mechanism by `xformOp:rotateXYZ`.** An authored rest
orientation is allowed where it belongs in the joint frames; continuous yaw or
pitch is a joint state. Euler writes bypass contacts, limits, solver state, and
the measurable `angle` output, then race the physics bridge.

### Coordinate contract for direction-tracking mechanisms

Before authoring or changing a tracker, write down and validate the complete
chain: world direction convention, mount-frame conversion, each joint's positive
axis/order, and the physical boresight of the final geometry. Do not assume the
vehicle's conventional `-Z` forward is the antenna or panel boresight: inspect
the composed asset. Derive the Modelica vector-to-angle equations from that
actual geometry, then verify live that the target vector, controller setpoint,
joint angle, and visible boresight agree. A controller's self-error/"locked"
output alone is not evidence of physical pointing; it can validate the same
wrong convention it commanded.

`PhysicsRevoluteJoint` is a generic mechanism, not a wheel marker. Do not infer
vehicle topology from an antenna, solar tracker, or robotic-arm hinge.

## 2. A prismatic joint CARRIES MOMENT

A `PhysicsPrismaticJoint` locks all three rotational DOF. It is a slider, not a
pin. This has a consequence that is easy to miss and impossible to see in a
screenshot:

Because it carries moment, a sprung leg has **two** ways to absorb a landing:
slide along its axis, which is the one you designed, or bend its angular lock,
which you did not. The second is always available, and the solver will take it
whenever a stray contact makes it cheaper.

### The failure: a second contact steals the load path

A suspension has exactly one intended load path — foot → spring → chassis. Give
the leg **any** second way to reach the ground and that path wins, because it is
rigid and the spring is not. A contact that only *sometimes* touches is worse
than one that always does: it latches on the first frame it grazes and never
lets go.

The diagnostic is a conjunction: `displacement` remains zero, the joint `force`
is absent or near zero under load, and the joint's angular lock is non-zero.
Measure the angular error together with the reaction force; angular error alone
is not enough because the joint solver is elastic.

**Ground clearance is a load-path property, not a styling one.** A raked box
strut's bottom corner hangs `half_thickness * sin(rake)` below its tip, so a
footpad centred on that tip clears it by almost nothing. Millimetres of margin on
a metres-long vehicle is zero margin: a fraction of a degree of tip puts the
strut on the ground. Size the foot so it is the *only* thing that can touch, by a
margin no small rotation can close — and beware that half-measures make it worse,
because a deeper foot demands a larger leg rotation to reach the ground, which
brings the strut down faster than the foot drops.

### Diagnosing it

Measure the angular lock directly from the two body orientations. A prismatic
holds `rot0 · localRot0 == rot1 · localRot1`; the angle between the two authored
joint axes is its constraint error. Then isolate contacts and inspect the
composed collider geometry. A raked box can contact the ground with a corner;
prefer a measured primitive or a deliberately authored proxy when the foot must
be the sole load path. Change solver and friction parameters only after the
geometry and contact ownership are verified.

### Where a joint's rest position sits

Anchors left unauthored are DERIVED from the transform hierarchy, which puts
displacement at exactly 0 in the authored rest pose. A leg authored `-0.8 .. 0.0`
therefore rests **on** its upper limit and travels one way only — by design: 0 is
the fully-extended pose the geometry is drawn in, and the ground can only
compress it.

So a stroke pinned at `0.0000` is not evidence the limits are backwards. Check
the load path first. Widening the limit to make a jammed leg move buries the
actual defect under a range the mechanism never needed.

**Anchors are also why you cannot freely move a body to fix clearance.** The
anchor is derived from the body's origin; move the origin and you move the
joint's zero, silently preloading the spring by the axial component of the shift.
Change the part's *extent*, or the mating part, not the sprung body's origin.

## 2b. Author the body as a FRAME, with real dimensions

### Make the body a frame, not a mesh

A prim that is both the rigid body and the geometry cannot host children, because
its shaping transform applies to them too. Give the body its own frame and put the
geometry inside it:

```usda
def Xform "LegPX" (prepend apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI"])
{
    # origin at the hull anchor, local -Y down the leg
    double3 xformOp:translate = (2.519, 1.388, 0)
    double3 xformOp:rotateXYZ = (0, 0, 25.0)
    uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"]

    def Cylinder "Strut" (prepend apiSchemas = ["PhysicsCollisionAPI"])
    {
        uniform token axis = "Y"
        double radius = 0.075
        double height = 7.05                       # spans local y 0 .. -7.05
        double3 xformOp:translate = (0, -3.525, 0)
        uniform token[] xformOpOrder = ["xformOp:translate"]
    }
    def Cylinder "PadPX" (prepend apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsCollisionAPI"])
    {
        double3 xformOp:translate = (0, -7.2, 0)   # 0.15 below the strut's tip
        ...                                        # + a joint, below
    }
}
```

Now every part of the leg is placed by how far down the leg it sits, in ONE frame,
next to its neighbours. "The foot is below the strut's tip" is something a reader
can see and a linter can compute. Place them in world coordinates instead and you
have written the same geometry twice, in two frames — and the copies drift.

### Author DIMENSIONS, not scale

`UsdGeomCube` has only a uniform `size`, so any real box has to be faked with a
non-uniform `xformOp:scale`. That scale then belongs to the prim rather than to
the shape, and everything downstream has to remember it: the frame is unusable for
children, the collider is a scaled shape rather than a measured one, and the part's
true dimensions appear nowhere in the file.

`Cylinder`, `Capsule`, `Cone` and `Sphere` carry `radius` / `height` — the
dimensions themselves, in metres. Prefer them, and prefer a `Mesh` with authored
`extent` to a scaled primitive. A strut is a cylinder; modelling it as a squashed
cube buys nothing and costs the frame.

## 3. Gravity is authored per scene

`UsdPhysicsScene` — the standard prim, `physics:gravityMagnitude` (scene units
per second squared) and `physics:gravityDirection` (a vector in the stage's
frame). Both convert at the boundary like every other authored quantity.

```usda
def PhysicsScene "PhysicsScene"
{
    vector3f physics:gravityDirection = (0, -1, 0)
    float physics:gravityMagnitude = 1.62
}
```

- **This is a lunar simulator.** Scenes are 1.62 unless there is a stated reason
  otherwise. The vehicles' drivetrains, struts and propellant budgets are sized
  for it.
- **ONE per scene.** Two prims that disagree are an authoring error and are
  reported as one; the last read wins, which depends on prim order.
- USD's sentinels are honoured: a NEGATIVE magnitude means "earth gravity", a
  ZERO direction means "the stage's down axis".
- **An orbital scene authors NO `PhysicsScene`.** Gravity there is per-body and
  position-dependent (`Gravity::Surface` + the celestial point-mass model). A
  flat vector would override that and pin every spacecraft to a fictitious
  "down". `assets/scenes/celestial/artemis_2_review.usda` is the worked example.

**Traction is gravity-dependent, and test thresholds must be too.** At 1.62 a
rover is traction-limited well below its drivetrain's `omega_max * r` ceiling
(measured 2.2–2.4 m/s against 4.8). A kinematic ceiling holds at any gravity — a
driven wheel cannot out-run its no-slip speed — but any floor derived from Earth
traction is simply wrong on the Moon.

## 4. A scene owns more than its entities

Anything a scene load writes belongs to that scene. Unloading despawns the
entities; **`SceneTeardown` unloads everything else.** Register teardown for every
scene-owned resource or override so a new scene starts from its own contract.

Add a reset system beside the code that writes the state:

```rust
app.add_systems(
    lunco_core::SceneTeardown,
    |mut commands: Commands| commands.remove_resource::<MySceneCache>(),
);
```

Which disposition is right depends on who owns the value:

- **REMOVE** state that only means something while a scene is loaded — caches,
  provenance records. Absence is its correct empty state.
- **RESTORE** state the app installs at start-up and a scene merely overrides.
  Gravity is the type case: a scene SHOULD override it, and must not leave the
  override behind. Removing it would leave the world with no value at all.

`SceneTeardown` grep-lists everything a reload restores. If you add
scene-derived state and do not register it, you have added a leak.

The shared physics owner clears `PhysicsHolds` and `PhysicsStepRequest` and
restores a zero-delta `Time<Physics>` at this boundary. The time owner keeps
`SimTick` and `Time<Fixed>` continuous across scene changes; it reanchors the
new mission epoch at the current tick and holds fixed cycles until the new scene
is ready. Any admitted fixed-time balance remains with that shared clock and
advances only after its causal holds clear. The controller/core owners clear
scene-keyed input and control-path state. Do not add an asset-local pause flag;
readiness and deliberate physics stepping must be owned by the incoming scene.

## 5. Reading the failure modes

| Symptom | Look at |
|---|---|
| `origin.is_finite()` panic in `obvhs` | a body reached ±inf; a raycast was issued from it. The *cause* is upstream — find the first `body has non-finite state` diagnostic |
| `[physics] body left the world: …` or `body has non-finite state: …` | first escapee names the mechanism that diverged. The required `physics.body_escape` Rhai policy pauses that dynamic joint island by default, including its colliders, while other bodies continue; inspect the first escapee to find the upstream cause. Bodies at the end of a lever arm (pads, wheels) escape first |
| `joint … starts violated by … rad` | a joint frame; see §1 |
| stroke reads exactly `0.0000` in every regime | a second contact carrying the load (§2). Measure the joint's angular-lock error before touching its limits |
| a spring loads the "wrong way" | almost never the joint. A jammed DOF and a reversed one look identical from the port; §2 tells them apart |
| a scene retains another scene's setting | a resource or override missing from `SceneTeardown` (§4) |
| a part is lying on the ground behind the vehicle | it declared its own body and no joint holds it (§6). `--validate` the asset, or `cmd("RunLint", #{})` the scene |

## 6. A part is not a body

`PhysicsRigidBodyAPI` declares a **body**, and the loader honours it wherever it
appears — ancestry is never consulted, because nesting-plus-joint is exactly how
a wheel is mounted (`Wheel_FL` under the chassis + a `PhysicsRevoluteJoint`).
So a prim that applies it and is jointed to nothing is a **free body inside your
vehicle**, and it leaves:

```usda
def Xform "Rover" (prepend apiSchemas = ["PhysicsRigidBodyAPI"]) {
    def Xform "Motor_FL" (prepend apiSchemas = ["PhysicsRigidBodyAPI"]) { … }   # ❌ falls out
    def Xform "Motor_FL" (prepend apiSchemas = ["PhysicsMassAPI"])      { … }   # ✅ part of the rover
}
```

**The rule.** Hierarchy is namespace; a **joint** is attachment.

Ownership follows from that, and it stops at every body boundary in BOTH
directions — USD and avian agree:

| what you author | what it becomes |
|---|---|
| collider with **no** body ancestor | standalone STATIC geometry |
| collider under a body | a piece of that body's compound shape |
| collider under a **nested** body | that nested body's piece — never the parent's |
| a nested body | a SEPARATE body; a **joint** attaches it, or it falls off |

The body prim may also carry `PhysicsCollisionAPI`. If it has a collision shape
and descendants, both the body-root shape and every eligible descendant shape
belong to the same compound. The root shape is identity-local to the body; its
authored transform is applied by the body's ECS transform once, while child
transforms are accumulated from the body frame. This remains true when the
body arrives through a reference or a prepared runtime-instance projection.

`physics:collisionEnabled=true` is an opt-in on an already-declared collider;
it does not apply `PhysicsCollisionAPI` by itself. Ordinary vehicle geometry
must carry both. `LunCoTerrainAPI` is the owner-level projector exception.
Raycast `PhysxVehicleWheelAPI` wheels are query-only and must not carry
`PhysicsRigidBodyAPI` or `PhysicsCollisionAPI`; physical wheels are the other
standard realization and must author both APIs on their collision geometry.
The `collision-enabled-without-api`, `raycast-wheel-collision-contract`, and
`physical-wheel-collision-contract` rules are errors, not runtime fallbacks.

Terrain collider ownership is mode-specific: `lunco:assetMode="mesh"` waits for
the USD-to-Avian mesh bridge, while `"dem"` and `"layered"` use the native
`SurfaceOracle` collider from `lunco-terrain-surface`. Do not author or project a
second mesh collider over DEM-backed terrain.

Initial poses are admitted against the live terrain after the USD pose bridge has
seeded every body. Convex colliders use their actual support geometry; composite
and non-convex shapes retain the conservative AABB check. A persisted pose that
overlaps after a terrain rebake is held as an authored error. Repair it with the
typed `TransformEntity` command or remove it with `DeleteEntity`; do not clear the
pending marker or silently translate the body in Rust.

Every renderable gprim under a composed `kind = "assembly"` vehicle must also
state who owns its collision contract. A supported enabled
`PhysicsCollisionAPI` shape is the ordinary owner. A raycast
`PhysxVehicleWheelAPI` prim owns a support query, not a collision shape;
physical wheel realization is selected by a standard revolute joint targeting
the wheel and requires the authored body/collision pair. Intentional decoration must explicitly author
`physics:collisionEnabled = false` or an inherited `purpose = "guide"`. When a
body has a `purpose = "proxy"` shape, its `purpose = "render"` geometry is the
visual description and is excluded from collision. Leaving visible vehicle
geometry unannotated is an error reported by `vehicle-part-collision-contract`,
not an invitation for a runtime fallback.

Hierarchy is namespace. **The joint is what attaches** — nesting a body without
one is the motor bug (`nested-body-no-joint`), and nesting one *with* a joint is
how a foot mounts on a leg and a wheel on a chassis. Both directions of that rule
matter: fold a nested body's collider into its parent's compound and one shape has
two owners, the compound holding it rigidly while its joint pulls it. They fight
every step until a body leaves the world.


- An internal part (motor, gearbox, battery, panel, lamp) = mass + geometry, **no
  body**. Its colliders fold into the host body's compound, its mass belongs to
  the host.
- A part that must move relative to its host = a body **and** a joint, authored
  together. That is what a mount (`AttachSpec`) writes, and it is why
  `mounting/demo_probe.usda` may keep its body.
- Same answer in every robotics dialect: URDF lumps a fixed-jointed link into its
  parent's inertia, MJCF welds a jointless nested body, and neither has a notion
  of a link inside a link attached to nothing. The complete rotational assembly
  inertia belongs on the moving body's authored inertia contract; a domain
  component that produces torque must not create a second hidden body or
  duplicate shaft state.

### Collision filtering

`JointCollisionDisabled` covers only the bodies named by a joint. For additional
contacts, author `PhysicsFilteredPairsAPI` explicitly; for many-to-many sets,
use `PhysicsCollisionGroup` with `UsdCollectionAPI`. Do not infer a vehicle-wide
self-exclusion: articulated mechanisms often need selected internal contacts.

Filtering is symmetric, targets may name a body or a collider under that body,
and invalid targets or same-compound pairs must remain visible diagnostics. The
filter is installed during `PhysicsSystems::Prepare`, before contact generation.
Verify contact itself in a scene test such as `scenes/tests/filtered_pairs.usda`
or `collision_groups.usda`, not only by checking subsequent motion.

## 6b. `purpose` — which geometry is the collision geometry

`UsdGeomImageable.purpose` is inherited and separates display geometry from
collision geometry:

| purpose | drawn | collided |
|---|---|---|
| `default` (nothing authored) | yes | yes |
| `render` | yes | only when the body has no `proxy` |
| `proxy` | no | **yes — this is the collision shape** |
| `guide` | no | never |

Use `proxy` for an authored collision shape alongside `render` geometry. A
`guide` prim is never a body or collider. `purpose` does not replace the frame
contract in §2b; proxy geometry still inherits its authored frame.

### Validate and run the scene

```bash
"$LUNCOSIM_BIN" --validate assets/vessels/rovers/skid_rover.usda
```

```
[usd/nested-body-no-joint] /SkidRover/Motor_FL — applies PhysicsRigidBodyAPI
inside the body </SkidRover> but no joint names it — …
```

and on the **loaded** scene, run the same rules through the verb:

```rhai
cmd("RunLint", #{}); query("GetDiagnostics", #{scope: "loaded_stages"});
```

The rules are authored in `assets/scripting/policy/lint_usd.rhai` — add one there
rather than in Rust. Pair the lint with a scene test such as
`scenes/tests/parts_attached.usda`, because lint cannot simulate motion. See
[`validate-assets`](../validate-assets/SKILL.md#the-rules-are-authored--the-lint-layer)
and [`docs/architecture/lint-substrate.md`](../../docs/architecture/lint-substrate.md).

### 6b.1 Vehicle-level stability acceptance

Mounted-part linting catches free bodies, but it cannot prove a rover will stay
upright under drive. Add a real flat-ground or production-terrain scene test
that settles the assembled vehicle, drives through its authored control path,
and reports:

- travel and enough fixed-step samples;
- maximum body tilt;
- missing or detached descendants and their worst relative displacement; and
- any domain output that is part of the vehicle acceptance, such as solar power
  and battery state.

On lunar regolith, inspect the force application height and the authored
`physics:centerOfMass` before changing solver parameters. If all driven wheels
reach the friction limit and the centre of mass is high above the contact plane,
the launch pitch moment is physical. Lower or correctly assemble the mass
distribution and rerun the test; do not turn a repeatable tip into a larger tilt
allowance or a rendering filter.

For reusable authored evidence, use
`physics_acceptance::sample`, `contact_report`, and the corresponding
`contact_acceptance`, `joint_distance_acceptance`, `motion_acceptance`, and
`settling_acceptance` helpers. Supply limits from the fixture or Twin model;
the library only reads the production telemetry and diagnostic surfaces. Use
`system_evidence`/`system_acceptance` to keep readiness and binding failures
visible beside physical measurements. Do not add a vehicle-specific acceptance
helper or change the solver to satisfy a threshold.

## 6c. What this engine does NOT read

Before authoring a schema because a DCC offers it, check that the importer
consumes it. The full table is in
[`docs/architecture/21-domain-usd.md`](../../docs/architecture/21-domain-usd.md).
Commonly mistaken fields include:

- **`PhysicsArticulationRootAPI`** — avian has no reduced-coordinate articulation;
  do not expect it to change this runtime.
- **`UsdGeomPointInstancer`** — the composed visual reader supports static direct
  renderable Gprim prototypes and standard array/mask semantics. Time-sampled
  arrays, animated prototypes, and arbitrary prototype subtrees fail visibly
  because their runtime sampling or visual multi-mesh batching is not yet
  implemented. Native **`instanceable = true`** scenegraph instancing remains
  outside the runtime projection.
- **`proxyPrim`** — not read; `purpose` on a sibling covers the case we have
  (see §6b).

An unconsumed field has no runtime effect. Use a supported schema or implement
the owner and its projection before authoring the field.

## Verify it, headlessly

`luncosim test` runs one authored scene plus its scenario deterministically, and
its exit code comes from a telemetry verdict:

```
"$LUNCOSIM_BIN" test \
    --scene scenes/tests/landing_legs.usda --max-ticks 500
```

A physics change is not done until a scene runs clean: **zero** `left the
world`, **zero** `starts violated`, and the scenario's own verdict PASSing. See
[`author-scenario`](../author-scenario/SKILL.md) for writing the verdict.

For raycast rover wheels, check the Cylinder axle and `lunco:wheel:headingAxis`
together: travel is steering-axis cross axle-axis, transformed by the physical
mount. Collinear axes are invalid. Align front/rear station names and the
Modelica drivetrain `forward_yaw_offset` with that travel direction; native
navigation uses −Z at zero offset, +X uses −π/2. Verify departure physically.
