--- 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::() { // ... 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.