---
name: lunco-ui
description: >
  LunCoSim UI architecture and panel implementation patterns.
  Use this skill whenever working on any user interface for the LunCoSim
  solar system simulation — adding panels, building dashboards, creating
  inspectors, spawning UI, telemetry displays, docking layouts, themes,
  or anything involving egui, lunco-workbench, or Panel.
  Also use when the user mentions typed commands, WidgetSystem, or 3D
  world-space UI. Even if the request seems simple (like "add a button"),
  use this skill because the panel registration and command patterns
  are project-specific and not obvious from Bevy alone.
---

# LunCoSim UI Architecture

## MANDATORY: Read Architecture First

Before implementing ANY UI, read:

```
crates/lunco-ui/ARCHITECTURE.md
```

It explains the full architecture, step-by-step panel integration guide,
and design decisions. This skill is a quick-reference summary.

## Core Principles

1. **UI lives in `src/ui/`** — domain crates have `src/ui/mod.rs` exporting a `*UiPlugin`. UI code never lives outside `ui/` directories.
2. **UI never mutates state** — all interactions emit typed command events (the `#[Command]` structs, triggered via `ctx.trigger(...)`) that observers handle. This makes the UI AI-native: AI observes the same command stream as humans and can emit identical commands.
3. **Panels are `Panel` impls** (the contract lives in `lunco_workbench_core`) — registered via `lunco_workbench_core::WorkbenchPanelAppExt::register_panel()`. The concrete shell drains that registration into its docking system.
4. **Headless must work** — removing UI plugins (Layers 3 and 4) leaves a functioning simulation. See `AGENTS.md` §4.1 for the four-layer architecture.
5. **Typography roles are shared.** `ThemePlugin` maps egui's built-in styles and `TypographyRole` styles from `Theme.typography` across egui surfaces in `ThemeApplySet`. Render systems outside the workbench should run after that set. Let ordinary widgets use the mapped Body, Button, Heading, Small, and Monospace styles. Use `TypographyRole::text_style()` for `RichText` and `TypographyRole::font_id(ui.style())` for custom painter text. Guidance and status messages use Body; Caption is for secondary metadata and compact status-bar summaries. Dense data roles are for chart labels, timestamps, and similarly compact values. Authored model text and world-space labels retain their content/spatial owner.

The workbench is deliberately split into contracts, reusable presentation,
optional guided presentation, and the concrete shell. `lunco-workbench-core` contains
the stable `Panel`/`PanelCtx`, `InstancePanel`, `PerspectiveLayoutPlan`, menu
registry, `WorkbenchSnapshot`, scheduling labels, and perspective command
payloads. It is safe for domain UI crates that need panel behavior or published
layout facts and does not pull the renderer or `egui_dock`. `lunco-workbench-guided-ui`
owns optional Rhai-driven HUDs, spotlights, coach-mark tours, and guided-recovery
surfaces; hosts add it explicitly after the shell. Its mission checklist can be
restricted with `GuidedObjectivesVisibility`; LunCoSim shows that checklist in
View while keeping tutorial hints, actions, spotlights, coach marks, and
recovery surfaces available over Builder and Editor. `lunco-workbench` is the
concrete shell: it owns
docking, egui/bevy integration, persistence, source editing, and shell-only
widgets such as icons and tree renderers. `lunco-workbench-widgets` owns the
shell-independent icon, text-editor, and tree helpers. Virtualized trees use
`tree::branch_header` for a fixed row stride; recursive trees use `tree::branch`
for an indented body, sharing the same disclosure state and controls.
`lunco-workbench-browser` is the
optional reusable Twin/Files feature: it owns browser state, standard panels,
and filesystem/library sections, including the asset-provisioning dependency.
Domain crates must not read the shell's private `WorkbenchLayout`; use
`WorkbenchSnapshot` for layout facts and typed workbench/browser actions for
navigation.

Universal runtime inspections should read an authoritative view model produced
outside egui. For ports, use `PortRegistry::entity_port_infos` so values,
units, ranges, source, authority, and writability come from backend owners;
sample at a bounded cadence when the data has no shared change marker. Writable
rows emit the existing typed command and validate against the metadata contract;
the panel must not infer policy from names or mutate port storage directly. If
the view caches metadata, its invalidation must use the owner-provided
`PortBackend::topology_key` and the durable owner-published
`PortTopologyRevision`. Providers publish it from component lifecycle
observers and change-filtered structural identity checks; Avian groups declare
their identity key and invalidation hook beside their membership predicate. Do
not use a broad scene revision or entity-count poll as a port-topology signal.
Large inspection surfaces must virtualize their fixed-height browser rows and
request live values only for expanded/visible bodies through the existing owner.
The USD Prim tree caches open rows and uses the
shared tree disclosure controls with preview-scoped identities; preserve
selection reveal, offscreen scrolling, and the typed display-mode commands.
The bundled-library browser consumes the asset manifest's revision, not a
per-frame hash of all paths. The normal sample path reads live values
through the registry rather than rerunning backend list/metadata callbacks.

For USD topology, extend the existing Connections projector in
`crates/lunco-luncosim-edit-ui/src/ui/connection_canvas/`. Default to the active
`SceneMountState` mount; Editor document focus is an explicit alternate source.
Read composed USD, including prims without ECS projections, independently of
Modelica compilation. Cache facts and consume stage deltas, never traverse in
paint. Start at the loaded root with direct hierarchy children; use cached
USD ancestry for drill-in and explicit descendant expansion. Preserve full property identities; causal edges have
arrows, acausal/joint edges do not. Never fabricate missing interfaces.
Scene selection uses mount-scoped entity targets, documents use `SelectUsdPrim`.
Use `Edit connections` / `OpenUsdSourceDocument` to establish the document
boundary without replacing the scene. The USD document owner resolves the exact
registered source on a worker and reuses its file-preparation lifecycle.
USD browser clicks use this source-only command too. `OpenFile` can open a
different workspace/Twin and is not an additional-preview command. Verify
source isolation through `assets/scenarios/tests/usd_source_isolation.rhai`,
launched by `scripts/api/test_usd_source_isolation.py` with an exact scene,
source and composed selection path in an owned production session. Successful
and rejected sources preserve the
active Twin and physics topology, and physics continues while a preview opens.
Use the composed USD reader's runtime-provider schema contract to display exact
authored interface references before runtime projection. Runtime owners validate
availability and types; the diagram does not admit connections to simulation.
Publish endpoint
failures with exact identities in Recent events, not in the diagram toolbar.
Use `diagram.layout` for authored Rhai layout policy over immutable graph
facts, including independent view keys and typed interface roles. Run it on bounded workers with source/scope revision fencing and the
Application/Visualization/Preparation context; validate complete finite
placements before rendering and apply saved placements afterward. The policy
may return a bounded display label and schematic accent role; consume both in
the view without modifying USD identities. Breadcrumbs,
Back and searchable Find system share cached USD ancestry. Double-click uses
`OpenConnectionNode` and `diagram.open.plan`: systems open USD child topology,
leaf Modelica programs use the existing schema editor, and Rhai/Python use
source. Navigation must preserve exact source identity and reject missing cards.
Modelica `OpenFile` resolves registered asset URIs on its worker through
`SchemeRegistry`, preserving the document owner and read-only library state.
Exercise the installed policy with `assets/scripting/tests/test_diagram_layout.rhai`.
Use `diagram.group.plan` for optional Rhai grouping over immutable program-source,
typed-port, topology and standard CollectionAPI facts; Rust validates and renders
its disjoint partition on the existing layout worker. Expanded frames and collapsed
summary cards preserve full USD topology. Resolve summary ports to their exact
original endpoint before authoring or inspecting a connection. Use the typed
`SetConnectionGroup`, `SetConnectionGroupCollapsed`, `RemoveConnectionGroup`,
`MoveConnectionGroup` and `SetConnectionGrouping` commands; group movement is one
journaled edit for all member placements. Manual membership, disclosure, exclusions
and the grouping switch belong to each existing named view. Salvage usable groups
and placements independently when an optional view file is damaged. Verify installed
policies through `assets/scripting/tests/test_diagram_groups.rhai` as well as the
layout gate; exercise collapse/expand, movement and rejection in the production API.
Persist named scopes and layouts in separate `.lunco-view.toml` project
artifacts using `lunco-doc::diagram_view` / `DocumentHost`, through typed view
commands. Each view has independent placement; USD files only own topology.
`Save views` / `Load views` use bounded asynchronous `lunco-storage` work and
recover valid entries from missing/corrupted optional view files with warnings
and automatic layout. Different sources or unsupported versions use the loaded
USD automatic view; duplicate file owners and stale loads reject.
Mouse port connections use journaled `ApplyUsdOps`, retain declared types and
unrelated links, and Save/Undo stay source-document-owned. Layout Undo/Redo
belongs to the view document. `Canvas.movable_layout` permits node arrangement
in read-only scene views. Consume `InspectConnectionDiagram` for exact source,
view IDs, layouts, rendered ports, and logical screen coordinates. Verify scene
inspection preserves same-prim feedback as edges. Drilling into a system
shows input/output/acausal interface terminals and child prims. `nodes[].key`
is the independent placement identity; `nodes[].path` stays the exact USD
prim for authoring and selection. `nodes[].role` identifies boundary terminals;
never infer runtime ownership from port names. `connections[]` retains exact
USD endpoints alongside their presentation keys. Verify input/input and
output/output boundary forwarding and rejection of terminal deletion. Verify
navigation, independent named layouts, file roundtrip, and mouse gestures in an
owned production API session, plus pure document/projector/interpreter tests.
Drag Library/Twin sources into a document diagram: USD becomes a reference;
Modelica/Rhai must land on an existing host prim. Models-palette drags retain
their typed authored port contract. Inspect `nodes[].programs` for backend,
source and resolver issues, including hidden children attributed to their
nearest visible ancestor. Drop naming/routing belongs to `diagram.drop.plan`
in `assets/scripting/policy/diagram_drop.rhai`; Rust validates its typed plan
and uses the existing journal batch. Verify terminal outcomes and placement
after source success with `connection_diagram_authoring.rhai`, including
missing-host and unsupported-source rejection. Exercise the installed policy
with `assets/scripting/tests/test_diagram_drop.rhai`.
Use `connection_diagram_wiring.rhai` for composed sink authoring, undo and
missing-target rejection in an owned document preview.
The contextual Connections inspector opens exact program facets with
`OpenConnectionNode.program_path`; an omitted program path retains normal
policy-driven double-click navigation. Show USD path/type, ports, peers and
Modelica/Rhai sources together beside the graph, using a floating panel on
compact widths. `SelectConnectionElement` validates exact cards/ports and
shares source-scoped scene/document selection. Selected nodes emphasize
incident links; selected ports emphasize exact endpoint matches, and unrelated
wires use shared disabled opacity. Use cached graph adjacency for peer rows.
Find searches prims, ports and program sources; reveal may navigate to the
exact hidden prim's parent. `NavigateConnectionDiagram` keeps view, scope,
expansion and viewport history; Back restores them after layout completes.
Verify tracing, navigation restoration and invalid identities with
`connection_diagram_explore.rhai`. Keep the header to source/mode and primary
actions, then navigation. Put named views, schema options and repository files
under View, variants under Details, and gestures under Help. Show USD dirty
state independently from view dirty state. Port dragging, click-to-connect
and the inspector Connect to picker must share endpoint/type validation; valid
targets show rings and invalid targets explain rejection before release. The
shared canvas validator rejects before EdgeCreated; ApplyUsdOps owns admission
and journaling. Distinct same-prim ports may author valid feedback.
Composed edits preserve the viewport through asynchronous layout. Verify native
dragging, undo and rejected directions with `connection_diagram_gestures.rhai`
in a visible Connections document. Supply `doc_id`, exact `source`/`sink` view
keys, `source_port`/`sink_port` and a same-direction `invalid_port` on the sink;
place the two cards within the focused viewport before running the gate.
Show acausal connectors as hollow diamonds with explicit
labels and separate counts; zero causal outputs does not mean a physical model
is disconnected. Show prim type and no authored ports for geometry/cameras.
Use `FrameConnectionDiagram` for Focus/Fit over the current view key; the render
boundary consumes its request through the shared viewport. Focus preserves
scope and topology and fits at most natural scale. Open uses the existing
double-click command. Verify exact card centering, complete-system fit and
missing-card rejection with `connection_diagram_focus.rhai`; inspect viewport
and node dimensions through the existing diagram query.

The Editor's `authoring_review` panel is the shared human-facing evidence
surface for authored/runtime inspection. Its target chain must keep `selected`,
`controlled`, and the active camera target as separate rows; do not collapse
them into a vehicle-specific status or infer control from selection. Render
retained `RuntimeDiagnostics` with the producer, severity, exact subject, and
message, and route a subject action through the canonical typed selection
command when a live `GlobalEntityId` exists. Inspection checkboxes use the
existing `DiagnosticVisualStore` leases for joints, frames, mass, forces, wheel
forces, and collision geometry. The panel is presentation only: measurements,
tolerances, provenance, candidate grouping, and preview/document navigation
remain authored Rhai over typed USD queries.

`default_slot()` seeds layout intent only before the first perspective is
active. After that, the active `Perspective` owns its slot declarations;
late panel registration adds the renderer without changing the current
presentation unless the panel declares `visible_in_perspective()` for the
active perspective. Use that contribution when a plugin-owned panel belongs
in another package's perspective, so the perspective does not need a dependency
on the panel package or a copied panel id. Cached user layouts still restore as
saved; contributions apply when a perspective builds its default layout.

Modelica preparation emits discrete lifecycle notices for its compilation queue,
solver-cache lookup, equation lowering, cache reuse, and elapsed preparation time.
The shared Modelica notice observer projects Info, Warn, and Error into Recent
status. Preparation notices are session/source-fenced and never solver results;
they must not release the simulation hold or change the model's time or outputs.

The workbench status history is one shared presentation surface: render Info,
Warn, Error, and Attention through the same responsive
level/source/message/action row. Its popup is compact, sized to roughly
half the available parent window and clamped to 420–960 logical px;
the message column consumes the remaining inner width rather than an arbitrary
fixed fraction. A new terminal RuntimeFault opens the popup automatically, but
its explanation is published as the ordinary Modelica Error event in history.
Selecting that row expands the complete message. Compile failures explain why
simulation did not start and include unmatched unknown names, categories, and
referencing equation rows when Rumoca can diagnose them. Step failures list
Modelica values not produced, their last accepted values, and sample and target times.
Use one scroll area with a stable `id_salt` in this popup so egui keeps one
history surface and does not report duplicate scroll-state IDs. Ordinary
warnings and non-terminal errors remain in history without opening the popup.
Diagnostic rows keep the shared column geometry, expose row activation through
the cursor and tooltip, and add the complete diagnostic as an optional body
under that row. Emit the existing typed action for Attention; do not create
level-specific row layouts or source-specific styling. StatusBus
coalesces consecutive identical discrete snapshots before this shared reader.
Keep the expanded history view discrete: do not append active progress to its
event list. Hide the entire event summary from the strip while that history
view is open, including its fallback to the latest discrete event; preserve
the strip click target so it can close the popup. Active progress shows a
compact notice anchored above the strip as a non-exclusive `UI_ORDER` overlay,
never an egui popup, so it cannot close a menu the user opens while loading. Scene progress is titled “Scene loading…” or “Scene unloading…” for a
clear transition; other progress keeps its source label. The complete
owner-written message wraps inside the card, and the status strip uses the same
concise scene label. Do not infer phase from free-form text. Terrain tile
streaming, optional terrain
refinement, and post-projection scene geometry stay published for readiness
consumers but drive the workbench loading notice only during an admitted or
active scene transition. This prevents camera movement after scene load from
reopening the loading notice. “Recent status details” opens the history popup,
which stays open when work completes. The compact notice disappears on
completion; do not stack it with the history view or auto-close a history view
the user expanded.

Stack application surfaces through `lunco-theme`, top to bottom: menus and
popups (egui `Foreground`), UI windows/dialogs/notices (`UI_ORDER`), the
general HUD (`HudTier::General`: sky clock, toasts, global notices), and the
specific HUD (`HudTier::Specific`: lesson or vehicle readouts). Create HUD areas
with `HudTier::area`/`HudTier::layer`; the workbench applies
`stack_hud_layers` once per pass. Never give a persistent overlay `Foreground`.
Authored runtime HUI paints before egui and stays below every egui tier.
Tutorial rings, coach/recovery cards, and completion prompts use
`lunco_workbench_guided_ui::GUIDED_OVERLAY_ORDER` (`UI_ORDER`); the lesson HUD is
a specific-HUD readout;
their scrims use the shared `GUIDED_SCRIM_ORDER` (`egui::Order::Background`).
Workbench menus and window controls are egui `Foreground` surfaces and therefore
remain above both tutorial layers visually and for input. The workbench measures
the live menu row and right-side control group; on compact widths it keeps File
and View direct and places registered domain menus plus Edit, Settings, Help, and
Time under one keyboard-reachable `More` entry. The direct and overflow surfaces
call the same menu renderers, so command semantics and callback state have one
owner. Build responsive menu measurements from the registry snapshot and group
scripted contributions only when their popup opens. Keep this one shared layer
contract; do not rely on system execution order
or give a tutorial surface a `Foreground`/`Tooltip` order that can cover
application controls. The tutorial
draw systems are chained within their shared layer, so their relative paint order
is deterministic as well.

Rhai-contributed workbench menus use the shared
`lunco_workbench::menu_popup_max_width` helper with the egui content viewport,
fix that width before laying out wrapped rows, and let short lists shrink
vertically instead of reserving an empty scroll region. Completion state uses
the shared vector `UiIcon::Check` and `UiIcon::Pending` with accessible status
text; do not render status words or font-dependent glyphs as a second status
system.

The workbench scopes only its private mutable layout out of the `World` while
painting. The authoritative `WorkbenchMenuRegistry` remains installed and is
read through an immutable frame snapshot, so layout resets do not clear menu
contributions. `PanelCtx` and `MenuCtx` trigger intents, together with shell
menu actions, go through `DeferredWorldTriggers` and are applied after the
egui pass restores the layout; do not call observer events directly from a
render callback when a typed context can queue the intent.

Egui developer overlays are owned by the shell's persisted
`WorkbenchAppearanceSettings::egui_debug_overlays`, off by default. Debug builds
offer the opt-in under Settings → Appearance. Do not enable diagnostics in
panel-local styles; the shell applies the preference to both theme styles on
all contexts before each egui pass.

Panel entries in the View menu use `PanelMenuGroup` for their primary workflow:
Builder owns live-Twin construction panels, Editor owns authored document
editing panels, and Lunica owns Modelica workbench panels. Put shared panels in
their primary home once; leave `Other` for integrations with no workflow home
and `Hidden` for internal panels that should not be user-toggleable.

Document-private viewport mounts retire at `DocumentClosed`, or when their
final preview lease ends with no workspace projection owner. Mount-set changes
prune their catalog entries before replacement scans; closing a preview must
not unregister a shared workspace authority.

For Twin-browser work, use `lunco_workbench_browser::BrowserQuery` as the single
transient search field. Sections filter their own authoritative view-models by
human-readable names/paths, retain matching ancestors, and emit the existing
typed navigation actions. Do not add a per-domain search resource or make the
browser search generated ids.
Shared scene-catalog choices retire closed-mount entries at `TwinClosed`; the
catalog owner invalidates scan generations while preserving application library
and surviving-mount entries. See the
[lifecycle contract](../../docs/architecture/61-scene-lifecycle-and-teardown.md).

Native USD Scene Files reads the published `SceneFileView`; never traverse or
stat files during paint or its main-thread input capture. Its single bounded
worker pins source owners/generations and a `TwinRootsSnapshot`, coalesces the
latest request, and rejects stale publication after source or mount retirement.
Keep valid rows during same-scope refresh, clear them on scope change, and make
preparation/admission errors visible without automatic retries. Verify the
active `TwinClosed` edge cancels the pending task, coalesced request and refresh
state immediately. `DocumentClosed` removes published USD rows at that edge.
Snapshots resolve authored logical Twin names against live mounts; publication
still requires the captured scope and revision. Verify the generic
`browser_retirement` and `scene_file_` lifetime/publication seams and the asset-owner closure
budget/error test; see the
[closure contract](../../docs/architecture/16-document-identity-and-collaboration.md#dependency-closure-separates-asset-traversal-from-usd-interpretation).

For hierarchy rows, use `lunco_workbench_widgets::tree::{branch, branch_header, leaf}` and
`tree::{label, selectable_label}` for row text. The shared renderer resolves
`Theme.typography.tree` and owns the
disclosure control, full-width row geometry, left-aligned label presentation,
persistent expansion identity, indentation, and the default depth-based
expansion policy. Domain panels own only their view-model filtering, stable
`egui::Id`, selection/loading state, and typed actions. Do not create a second
`CollapsingHeader`/`CollapsingState` path for a tree, or duplicate selectable-row
sizing and alignment at call sites. Do not move domain selection or loading
policy into the shared renderer. Search may force matching branches open for
the current frame; ordinary expansion remains persistent UI state.

Selectable tree rows use `tree::selectable_label`, even when they support drag
and drop or reserve trailing value columns. Wrap the shared label as the drag
source instead of replacing it with a direct `Button::selectable` call.

Project-owned settings are not user-global settings: read the active Twin's
manifest through the workspace resource and emit a typed event for changes.
Workbench hot-exit documents, tabs, and dock windows are scoped to the active
Twin. With no active Twin, use the host's startup layout and skip workspace
state load/save; do not restore loose Editor or Modelica tabs from an
app-global no-folder session.
Workspace snapshots load, prepare file-backed documents, serialize, and save on
Bevy's task pool through `lunco-storage`. Domain codecs restore documents
through their existing registries and lifecycle events. Document-backed view
tabs can remap many saved views onto one canonical document; the codec chooses
whether unmatched tabs are retained or dropped, while stable singleton-instance
panels keep their own IDs.
For the missing-asset consent flow, the popup's unchecked negative checkbox
means "show next time" and persists through `twin.toml [downloads]`; do not
add a second global settings key for it.

The Entities panel follows `SceneMountState::active_root` through the shared
`UsdSceneRoot` ancestry helper. Membership follows scene hierarchy through
nested grids and remains independent of physics frames. Additive mounts and
render-only previews are excluded. Membership is an application-owned scene
fact, not a Twin preference. Clear the derived tree on active `TwinClosed` so
outgoing rows cannot linger.

The native Updates submenu is content-sized by the workbench's shared menu
container. Keep its identity block explicit (`Version`, `GitHub Actions build`,
and `Update channel`), derive the run/attempt label only from the CI-stamped
nightly version, and show an explicit unavailable state for local builds. Do
not present a source SHA as the GitHub build number or force a feature-local
minimum width.

Generated USD runtime scene edits use the same Twin-owned settings boundary:
`usd.runtime_persistence` is one boolean opt-in for both reading and writing
the `.lunco/runtime` cache. The Settings menu reads the USD owner's policy and
emits `SetTwinSetting`; it must not register a global autosave section or
invent a second persistence path.

Camera labels are a shared projection owned by
`lunco-usd-bevy::camera_switch::camera_display_labels`. Reuse it in workbench
camera lists, the USD/entity trees, and Inspector; keep the full USD path as
the selection/tooltip identity. Unique leaves are compact, duplicate leaves
gain nearest-owner context, generated ID suffixes stay out of primary text,
and only an unavoidable normalized collision gets an ordinal.

Document-scoped editor panels use the existing `UsdPreviewId` session as their
view-model key. Keep each open session's tree, canvas, authored Inspector data,
joint/animation state, authored layer, and projection generation isolated;
the Editor perspective opens the prim tree in the upper-left pane and keeps the
Twin Browser in the separate lower-left pane, so document choice and authored
hierarchy remain visible together. The prim tree consumes the full width of its
pane and uses immediate structural collapse rendering; do not add a nested
auto-shrinking scroll region or animated body that can paint stale outlines
over neighbouring rows.
All hierarchy rows use `lunco_workbench_widgets::tree::{branch, leaf}` and
`tree::{label, selectable_label}`; depth-based initial expansion uses
`tree::default_open_at_depth`. The Ports entity browser follows the same
contract as the Twin, Entity, USD, Modelica, library, and telemetry trees. Raw
`CollapsingHeader` is for non-tree sections, not a second tree implementation.
The Prims rows reuse the Entities selectable-row presentation
and expose preview-scoped Visible, Invisible, and Contour controls at the
trailing edge. Those controls dispatch
`SetUsdPrimDisplayMode`; the viewport projects the typed intent and the render
binder owns wireframe rendering. Strip leading decorative bullet, circle, and
square markers from the row's presentation label without changing the authored name.
The viewport panel paints the session selected by the focused `UsdPreviewViewId`. Views share that
session projection while keeping independent camera/render-target state. The
viewport applies `UsdPreviewRenderBudget` to visible view targets (2048 px per
axis, 4,194,304 pixels per view, and 8,388,608 visible pixels per frame by
default) and leaves hidden cameras inactive. The generic ECS selection is a
focused-session projection and must be restored from editor-owned session
selection on focus changes. Dispatch edits with the session's explicit
document, authored layer, and generation through the typed USD command surface.
The full-width Prims tree owns a single vertical scroll surface; a newly
selected primary row opens its ancestors and scrolls into view, while a stable
selection leaves manual scrolling untouched.
For path-based editor selection, use `SelectUsdPrim` with the focused preview;
never resolve a USD path globally across live and preview projections.

The USD visual preview is an egui image over an offscreen camera, so its
selection surface must publish image-local primary clicks with Shift/Ctrl
modifier state. The editor selection owner maps that position through the
focused camera and ray-casts only the preview's composed hierarchy, selecting
the nearest non-empty `UsdPrimPath` ancestor. The viewport's exact image rect
is also the sole geometry source for render-target sizing and gizmo handle
mapping; do not use the surrounding toolbar-bearing panel rect. When a gizmo
handle owns a primary drag, the preview camera's competing pan path is
suppressed so the pointer remains in gizmo mode.

Scene click ownership is perspective-scoped. Read the shared
`lunco_interaction_core::SceneInteractionMode` contract: View/simulation owns unmodified
clicks for possession, while Shift/Ctrl clicks remain explicit selection or
removal intents; editor-facing perspectives own unmodified clicks for
selection and gizmos. Do not add a second per-crate mode flag or let global
pointer observers infer ownership from the hit entity.

The workbench host is also the app-level semantic input surface. The shared
contract is owned by `lunco-control-core`; editor actions
such as Cancel must consume the shared `UserIntent`/`InputBindingsSettings`
path, not a raw `KeyCode` or an assumption that an isolated USD preview has a
local avatar. `CancelIntent` suppresses the action while egui owns keyboard
focus, preserving text-field editing.

Avatar presentation has one wall-clock interaction cadence. Free-avatar port
writes, locomotion, and camera look application belong to
`lunco_time::InteractionSchedule`, not `FixedUpdate` or a direct `Update` pose
writer. Render-frame pointer input may be buffered before that schedule, but
the interaction step must consume it once alongside movement so pause and
simulation rate changes cannot split orientation from translation.

For authored UI workflows, use the controller's `InjectWindowInput` command and
the Rhai helpers in `prelude/input.rhai`. They enqueue typed key, pointer, and
scroll events through the same Bevy aggregate `WindowEvent` and typed input
messages produced by `bevy_winit`; picking, egui, focus, and semantic bindings
therefore keep their normal owners. Rhai composes chords, clicks, drags, and
workflow timing. Do not call a scene tool directly, synthesize a
`ScenePointer`/intent event, or create a second UI automation command for a
particular panel. Coordinates are logical primary-window pixels, and the
command is local to the client window.

In View, a click on a vehicle part resolves through the enclosing authored
`ControlBinding`/`MobilityRoot` before considering nested component input
surfaces, so the vehicle remains the possession target. Standalone non-avatar
input surfaces remain valid direct targets.

For interactive reusable-assembly authoring, follow the
[edit-usd-assembly runbook](../edit-usd-assembly/SKILL.md): the production
window must remain headful and visible to the user, each coherent typed edit
must be followed by a screenshot the agent inspects, and user feedback is a
required checkpoint before the next material edit or save. Do not turn a panel
into a direct file writer or a second session/state owner.

World-space vehicle trails are transient render presentation, not UI-owned
state. Read solved wheel contacts after `WheelRaycastResultsSet`, retain bounded
stroke history, and clear it on frame changes and `SceneTeardown`. A moving
endpoint provides sub-spacing continuity; missing wheel support starts a new
stroke, including airborne and inverted motion. Use the physical wheel's full
width. DEM contacts resolve their `ColliderTileOf` owner and reuse streaming
`SurfaceCurveAnnotation` segments painted on terrain fragments; ordinary static
supports use the shared contact-plane ribbon builder. Do not derive tracks from
root motion, visual wheel roll, controller input or authored routes. Inspect
`InspectVehicleTrail` and exercise `vehicle_trail_contact.rhai` through the
production API; verify the exact target scene visually. See the canonical
[motion trail contract](../../docs/architecture/50-usd-driven-visuals.md#motion-trails-are-bounded-physics-history)
for history, admission budgets and publication fencing.

## Runtime-authored HTML/CSS surfaces

For a Twin-facing HUD, telemetry card, progress overlay, or simple runtime
control that should change without a Rust rebuild, use the dedicated
[runtime-ui skill](../runtime-ui/SKILL.md) and
[`docs/architecture/runtime-authored-ui.md`](../../docs/architecture/runtime-authored-ui.md).

This is a separate presentation path built on HUI/Flair. It uses the generic
`EngineExposures` capability registry, the existing `WorkbenchEguiHost` camera,
and the workbench's authoritative dock/pick geometry. The host is the persistent
Bevy UI layer: it paints into the scene camera's shared cleared main texture,
with its MSAA/HDR key synchronized to the active scene camera. This keeps UI
state correct across camera replacement and perspective changes without a
load-preserving accumulation target. It does not replace
`Panel`/egui, create a second UI camera, or permit templates to mutate domain
state. Use this skill for workbench panels and use `runtime-ui` for authored
HTML/CSS surfaces; do not create a hybrid shim for one widget. The
manifest controls the retained surface's outer rectangle: the runtime pins its
minimum and maximum size to the resolved placement and clips overflow, while
the authored CSS/profile must fit its contents inside that boundary. The
`lunco-ui::modal` host is the canonical owner of queued modal outcomes,
scrim, focus, Esc dismissal, and typed `CloseModal` dispatch. Base HUI does not
provide those dialog semantics. The separate `bevy_hui_widgets 0.6.0` crate
offers primitive input, slider, and select mechanics, but it is not included in
LunCoSim and does not define clipboard, validation, keyboard-navigation,
accessibility, or modal semantics; do not route a rich editor through it
without a new typed contract and acceptance tests.

For lander control cards, keep GNC identity and state on the existing authored
USD boundary: resolve the column-zero `lunco:ui:schemaNode`, then read its
`ModelicaModel`/`SimComponent` lifecycle and authored causal ports. Do not use
the generic `Autopilot` actor as a lander GNC indicator, infer GNC from a prim
name, or create a second status store. Add operator channels with
`LunCoTelemetryAPI` on the guidance component so the compact card and the
telemetry browser read the same SignalRegistry samples. The card must expose
unavailable, compiling, ready, active, paused, handoff, and failure states;
absence of an actuator sample is not permission to hide the authored surface.
The runtime exposure producer must additionally scope card roots to the active
`SceneMountState` root. Preview, additive, and outgoing scene entities are not
operator HUD sources, and a repeated ECS projection of one composed `(stage,
prim path)` must not fill a second manifest slot; report the duplicate at the
projection owner and retain one active identity. Scene-derived exposure
namespaces are hidden synchronously on `SceneTeardown` so deferred despawns
cannot leave the outgoing card visible; the next active scene repopulates them,
while retained application facts such as `camera-status` remain available.

The Modelica diagram's `Show nets` checkbox is the Connections legend's single
workbench/egui presentation control, backed by the per-tab
`lunco-canvas::Canvas`. It hides rendered and interactive connection edges
without changing authored topology. Keep the legend and this dynamic graph
control in the canvas panel; HUI/Rhai does not provide the dynamic list and
graph-state contract it would require.

Canvas-owned diagram overlays must stay inside the owning leaf: direct painting
uses the canvas clip rectangle, while an `egui::Area` uses `constrain_to` with
the measured leaf rectangle. Non-interactive overlays do not claim pointer
input, and modal dialogs use the shared `lunco-ui::modal` host.

## Adding a Panel

```rust
use lunco_workbench_core::{Panel, PanelCtx, PanelId, PanelSlot};
use lunco_workbench::WorkbenchAppExt;
use lunco_ui::prelude::*;

pub struct MyPanel;

impl Panel for MyPanel {
    fn id(&self) -> PanelId { PanelId("my_panel") }
    fn title(&self) -> String { "My Panel".into() }
    fn default_slot(&self) -> PanelSlot { PanelSlot::RightInspector }

    fn render(&mut self, ui: &mut egui::Ui, ctx: &mut PanelCtx) {
        // READ state — view-model resources / selected components via the
        // ctx, never raw `&mut World` scans, never mutate.
        if let Some(sel) = ctx.resource::<UiSelection>() {
            // ... read sel ...
        }

        // EMIT a typed command event — never mutate directly.
        if ui.button("Action").clicked() {
            ctx.trigger(MyCommand { /* ... */ });
        }

        // Other supported mutations are queued through the same context:
        // ctx.set_resource(MyResource::default());
    }
}

// Register in ui/mod.rs through the concrete shell:
// app.register_panel(MyPanel);
```

## What NOT to Do

| ❌ Don't | ✅ Do |
|----------|------|
| Mutate resources directly from UI | `ctx.trigger(TypedCommand { .. })` (or `ctx.defer`), let observers handle it |
| Put UI code in `lib.rs` or outside `ui/` | All UI in `src/ui/` subdirectory |
| Use `world.query()` every frame for graphs | Use `WidgetSystem` for O(1) cached queries |
| Copy a SignalRegistry history every graph paint | Use the visualization-owned history fingerprint cache; copy only at the egui plot data boundary |
| Reproject every canvas edge every paint | Cache screen geometry by scene generation and viewport key; keep selection/tool state live |
| Walk the dock tree once per anchor group | Publish all slot anchors from one dock traversal |
| Build custom docking/themes | Use lunco-workbench — it's already there |

## Discovering Existing Commands

Commands are typed structs marked `#[Command]`, handled by observers
marked `#[on_command(TypeName)]` (both from `lunco_core`). To find what
commands exist:

```bash
# Find all command observers
grep -rn "#\[on_command(" crates/

# Find all command struct definitions
grep -rn "#\[Command" crates/

# Find where a command is emitted from UI
grep -rn "\.trigger(" crates/
```

To add a new command: define a `#[Command]` struct + `#[on_command(..)]`
observer in the relevant domain crate (see the `test-via-api` skill's
"Add a command" section for the full pattern).

## When to Use WidgetSystem

| Use `WidgetSystem` | Use raw queries |
|-------------------|-----------------|
| Queries same entities every frame | Reading 1-2 resources |
| 10+ query fields | Simple UI, minimal ECS |
| 100+ rendered items | Infrequent panels |

## File Structure

```
crates/lunco-ui/           ← mechanisms (WidgetSystem, typed commands, 3D UI)
crates/lunco-*/src/ui/     ← domain-specific panels
```

Wheel-track history uses `VehicleTrailSettings.max_points_per_wheel` (default
32768, about 16 km at half-metre spacing). Terrain annotations preserve retained
history through adaptive spatial subdivision; they never shorten a lane because
a root cell is crowded. Moving heads and retirement emit stable segment deltas;
`InspectVehicleTrail.annotation_edits` exposes the consumed update count.
Dirty-range uploads preserve the resident texture and its material bindings.
Budget errors are terminal publication diagnostics.
Validate long curved paths as well as live contact, and inspect actual GPU
output after image capacity grows.
