--- name: edit-usd-assembly description: > Create or modify a reusable LunCoSim USD assembly such as a rover, lander, payload, or sensor mount through the live headful Assembly Editor. Use when the user must see each change in the running window, give feedback between edits, and the agent must inspect the result with screenshots. Use the existing `assembly_edit` Rhai tools and typed USD commands; decompose reusable or articulated parts into separately testable USD components and compose them through typed references, datums, joints, and variants. Do not edit USDA text or ECS state directly. --- # Interactive USD Assembly Editor For the short, reusable quality checklist (typed operations only, named frames/sockets, independent component framing, SI dimensions, and fail-closed visual requirements), also read [`assembly-quality`](../assembly-quality/SKILL.md). For the mandatory one-component-at-a-time editing cycle and the cross-tool CAD/CAE practice mapping, also read [`interactive-component-authoring`](../interactive-component-authoring/SKILL.md). This is the human-and-agent workflow for editing an existing assembly. It is not a second authoring API. The running production `$LUNCOSIM_BIN` window is the shared workspace: the user sees the focused Editor preview, the agent drives the same typed commands, and the agent inspects screenshots and typed state after every coherent edit. ## Mandatory interactive mode Start in **headful windowed mode**. Launch the production binary with an explicit API port and without `--offscreen` or `--no-ui`: ```bash "$LUNCOSIM_BIN" --api 4127 ``` Use the existing headful session when one is already running. If no graphical display is available, stop and report that the required interactive workflow cannot be observed; do not silently switch to headless or offscreen mode. The normal opening sequence is: ```bash curl -s -X POST http://127.0.0.1:4127/api/commands \ -H 'content-type: application/json' \ -d '{"type":"ExecuteCommand","command":"OpenTwin","params":{"path":""}}' curl -s -X POST http://127.0.0.1:4127/api/commands \ -H 'content-type: application/json' \ -d '{"type":"ExecuteCommand","command":"ActivatePerspective","params":{"id":"editor"}}' ``` Wait for `/api/ready` to report `ready:true`, `world_hold:false`, and `pending_count:0` after opening the Twin. Then query `ListOpenDocuments` and use the returned id with the `assembly_edit` helpers. Do not replace these steps with a guessed `doc` or a direct file path in an authoring command. The user-visible window is part of the acceptance surface. Keep one session alive while iterating. Do not apply a chain of unobserved edits and reveal only the final file. After each coherent change set: 1. query the command acknowledgement and current document generation; 2. let the normal projection update the focused preview; 3. query the affected composed USD prim or session state; 4. capture a screenshot with `CaptureScreenshot` and inspect the PNG with the image viewer; and 5. show the result to the user and take feedback before the next material change or final save. The edited document must be the owner of the part being changed. A component edit is authored in that component's own USD document; the assembly receives only references, instance transforms, variants, and host wiring. Once the typed operation is accepted, live projection automatically refreshes every already-mounted dependent stage that references the changed `twin://` layer. This refresh is dependency-scoped (unrelated stages are untouched), preserves the existing preview camera, orbit/zoom, selection, and view tab, and does not close/reopen the preview. Do not add a manual reload loop or duplicate the component under the assembly to force a visual update. If a Twin has a deliberate local policy, it may register the generic `usd.component_refresh` Rhai hook and return `#{action: "propagate"}`, `#{action: "defer"}`, or `#{action: "reject"}`. Invalid hook output is a visible diagnostic and does not silently fall back to a different authoring path; unregistering the hook restores the built-in automatic propagation policy. When opening a file from inside a registered Twin, verify that `InspectUsdViewport` reports the Twin-relative `stage_asset_path` and the component URI in `recipe_layers`. The viewport resolves the assigned Twin authority before loading the stage; synthetic `__viewport_*` authorities are only for files outside registered Twins. If the authority or layer closure is wrong, stop and report it before editing—the assembly cannot refresh a different asset identity. Use a project-local ignored artifact path such as `target/assembly-editor/lander-after-mount.png` for screenshots. Do not create an alternate screenshot or state protocol in `/tmp`, and do not treat a screenshot as a substitute for typed USD or runtime verification. For example, save the current visible frame through the existing screenshot command: ```bash mkdir -p target/assembly-editor curl -s -X POST http://127.0.0.1:4127/api/commands \ -H 'content-type: application/json' \ -d '{"type":"ExecuteCommand","command":"CaptureScreenshot","params":{"save_to_file":true,"path":"target/assembly-editor/lander-after-mount.png"}}' ``` Read that local PNG with the image viewer before reporting the checkpoint. ## Respond to “the selected part is wrong” Treat the visible selection as the user's referent, not a filename or display name guessed from conversation. Read `selection_context()` first, then the matching `viewport()` and `describe(doc)`. Require the focused preview, exact selected paths, and matching ready document/projected generations. An empty, stale, or ambiguous selection is a clarification point; a singular request with multiple selected parts must not silently choose the primary entry. Inspect the composed selected prim and capture the visible frame. If the problem is not established by those facts and the user's requirements, ask what should change before inventing a design. Explain the scoped correction in ordinary language; users do not need to supply internal document handles. Build a proposal from the captured identities and generation. Immediately before committing, reread selection and document state. If focus, selected paths, edit target, or generation changed, do not retarget or commit silently: reinspect and resolve which selection the request applies to. Proposals carry explicit paths and are not dynamically rebound to whatever is selected later. After commit, wait for the matching projected generation, inspect composed facts and the screenshot, and present the result for revision or undo. Use the same document's `UndoDocument`/`RedoDocument` for feedback. Save remains a separate approval gate. The authored `selection_ai_workflow` graphics scenario exercises context isolation, selection-change detection, proposals, stale generation rejection and undo; inspect its real production verdict, not just frame completion. It does not substitute for user acceptance of a real design. ## Choose Editor or Builder Use **Editor** (shown as `✎ Editor` in the perspective switcher) for one specific reusable assembly: open its USD document from the Twin Browser, focus its isolated preview, and edit the authored prim tree. This is the path for rover, lander, payload, and sensor work. Editor does not show the live Entity list or spawn palette because those operate on the mounted Twin rather than the selected document. Use **Build** for general base composition: spawn and place complete USD assemblies in the live Twin, and select them as one element. The group boundary is the authored compound root projected through USD `PhysicsRigidBodyAPI` and the existing `SelectableRoot`/`MobilityRoot` markers. Drill into the explicit assembly document in Editor to modify its internal parts. Never add a second group table or identify members by name prefixes. `◉ View` is a separate live-Twin perspective for operating the simulation. `USD · View N` tabs are separate presentation views inside Editor: each has its own camera and render target while sharing the one explicit USD preview stage. Neither kind of view is a document identity; query the explicit handles before editing. When leaving an Editor field to control a possessed vehicle in View or Build, click the main 3D scene once. That scene press is the workbench's typed focus handoff: it clears retained egui text focus while preserving capture for a field that is still active. The controller then receives the normal `InputBindingsSettings` → Leafwing `ActionState` → authored `ControlBinding` path. Do not bypass this boundary with raw-key reads or a second vehicle input path. ## Decompose the assembly like a lightweight CAD product Before changing geometry, write the design intent as contracts. Separate facts supported by public or global references from Twin study assumptions, and give each independently reusable, articulated, or domain-owning part an explicit owner. A useful split is: - the assembly file owns the vehicle datum, placement, references, collection membership, host-facing joints, and cross-component connections; - a component file owns one reusable part's local geometry, mass/collision envelope, mount datums, parameters, and internal mechanism; - a Modelica scope owns domain equations, while a Rhai scenario owns mission sequencing and limiters; - Rust owns only generic typed USD, physics, projection, and solver seams. Do not split merely to create ceremony. Keep a part in the assembly when it is only a one-off visual detail with no independent mount, mechanism, runtime contract, or useful test. For an articulated or reusable part, use a separate file under the Twin's `components/` tree with one explicit root and one clear composition boundary. The component should be usable without the final assembly, while the assembly should be understandable from its manifest and references without opening every implementation detail. For every component, create the smallest useful contract before detailed geometry: required prim names/types, local frame and mount datum, dimensions or envelope, mass/inertia ownership, collision policy, public parameters and their units, and any known limits. Put the component's read-only requirement report and boundary cases beside its Rhai authoring tool. The assembly gets a second contract that checks counts, placement, symmetry, clearances, references, joint endpoints, and cross-component wiring. A component passing alone does not prove the assembled vehicle is correct. Use this live loop for each component and then for the assembly: 1. Open or create the exact USD document in the headful Editor and inspect its document id, authored layer, target, and generation. 2. Build a pure Rhai typed-op plan. Use `AddPrim`, standard schemas, `SetAttribute`, `SetTranslate`/`SetRotate`/`SetScale`, relationships, and connections. Let the document owner maintain `xformOpOrder`; never inject that property manually. 3. Review and commit one coherent component change, wait for projection, query the affected composed prims, and capture/inspect a screenshot. 4. Run that component's requirement test in the same live process. Repeat the same session for the assembly integration, then run the cross-component suite against the composed root. Do not use a second simulator launch just to run a component or assembly test. 5. Save each component and the assembly only after the visual and typed checkpoints are acceptable. Record the exact files and known gaps. Use the Editor's **Authoring Review** panel during those checkpoints. It is a read-only consumer of the existing selection, possession, active-camera, runtime-diagnostic, and diagnostic-lease owners. It deliberately shows selected, controlled, and camera-target identities separately and can select a live diagnostic subject through its stable entity id. For dimensional checks, run `authoring_measurements::requirement_report(doc, requirements)` with exact USD paths and units; do not turn a screenshot, a display name, or a guessed bounding box into a requirement. The report must retain every check and treat missing/stale/unavailable evidence as a visible result. When a component is deliberately a global reference, validate the referenced asset's contract and keep local opinions limited to its instance transform, mount metadata, variants, and host wiring. When a new Twin-owned component is needed, author it through the live Editor and save it as a separate file; do not paste a flattened copy of a reference or import a private CAD/FreeCAD file as an undocumented authority. ## Variants and levels of detail Use a USD variant set at the component or assembly boundary for real configuration choices such as `stowed`/`deployed`, `payload_class`, or an alternate equipment package. Keep the base contract common and place only the opinions that differ inside the variant. Select variants with the typed `SetVariantSelection` path after the referenced subtree is materialized, then rerun the component and assembly checks for every supported selection. Do not emulate a variant with duplicate top-level parts, name suffixes, or a permanent invisible placeholder. If the live typed surface can select existing variants but cannot create a variant set or author variant blocks, report that as a Rust/typed-editor capability gap; do not fake canonical USD composition with visibility flags. Reference-list replacement and authored metadata such as `kind`/`defaultPrim` are available through typed editor operations; keep the selected layer and generation explicit when using them. Model at contract fidelity, not manufacturing detail. Use standard primitive geometry for the silhouette, mounting envelopes, collision surfaces, and visual features that the requirements actually inspect. Add meshes or parametric detail only when it changes a requirement, a mating interface, a physics envelope, or the visible identity of the vehicle. This keeps the live CAD loop fast without making the component a placeholder. ## Open the exact document and preview The document registry and OpenUSD composition system are authoritative. Never guess a document id, preview id, prim path, or edit layer from a name or from the active simulation viewport. 1. Open a Twin folder with `OpenTwin` (its path contains `twin.toml`), open a source with `assembly_edit::open(path)`, create a new assembly with `assembly_edit::new_document()`, or fork an existing document with `assembly_edit::fork_document(source, name)`. These are the normal async document lifecycle paths; `LoadScene` is only for mounting a scene. 2. Query `ListOpenDocuments` and select the returned `DocumentId`. IDs are process-wide live handles shared by all document domains. Confirm the returned kind and file origin; never derive an ID from an entity or reuse a number from a previous launch. A fork has its own fresh handle. 3. Query `assembly_edit::viewport()` and capture a screenshot when the user asks what is visible. Correlate the focused preview/view and visible tabs with `ListOpenDocuments`; use the returned explicit handles, never a title. 4. Use `assembly_edit::describe(doc)`, `inspect(doc, path)`, and `resolve_target(doc, path, edit_target)` to read composed topology, layer ownership, generation, and the legal target. Use `sync_document` when an already-known generation can be advanced by a typed delta. 5. Select the `Editor` perspective from the title-bar switcher, or activate it through the typed `ActivatePerspective { id: "editor" }` command, then open an isolated preview with `OpenUsdPreview { preview, doc_id, edit_target }`; the preview's primary view is exposed as an instance-backed dock tab. Use `FocusUsdPreview { preview }` when changing the visible document; it foregrounds that tab. Every panel and selection must remain bound to that `UsdPreviewId`. 6. For a second 3D view of the same assembly, use `OpenUsdPreviewView { preview, view }`. It creates a new camera/render target over the existing projected stage; it does not reload or duplicate USD. Opened view tabs can be dragged to a dock edge to create a split. Use `FocusUsdPreviewView` for the exact view and `CloseUsdPreviewView` when finished. The runtime parks cameras for hidden tabs and only sizes visible view targets from their measured dock rects. The USD viewport's `UsdPreviewRenderBudget` caps each target at 2048 px per axis and 4,194,304 pixels, and caps visible views at 8,388,608 pixels per frame by default. The active Twin's workspace restores one document per file origin and recreates saved view tabs over that same preview session, including each view's camera and presentation settings. Reopening a file reuses its document; use **Open view** to add another perspective. Failed or stale USD view tabs are removed during restore rather than left empty. These are presentation budgets, not authored USD values; invalid zero limits leave the target inactive. Wait for `InspectUsdViewport` to report `projection_ready: true` before selecting a prim or issuing an authoring command. The flag becomes true only after the preview root and all projected descendants have cleared their USD projection and asynchronous mesh phases; `projected_generation` is valid for edits only at that boundary. Navigate the focused view with primary/left-drag pan, secondary/right-drag orbit, middle-drag pan, and wheel zoom. The toolbar and agents use the same typed commands: `SetUsdPreviewViewMode { view, mode: "visual"|"text" }` and `SetUsdPreviewTextLayer { view, layer: "authored"|"composed" }` switch the presentation of the same preview session. Visual mode owns the shared projected stage and camera controls; Text mode is a read-only, generation-matched USDA snapshot. Switching modes never opens another document or resets selection/camera state. Use the text-layer command only while inspecting authored or composed source; mutations still go through typed USD operations. `SetUsdPreviewProjection { view, projection: "perspective"|"orthographic" }`, `PanUsdPreviewView { view, delta: [x, y] }`, `ZoomUsdPreviewView { view, factor }`, `FrameUsdPreviewView { view }`, and `ResetUsdPreviewView { view }`. These are view presentation operations; they do not author USD camera or transform values. Pan converts logical pointer deltas through the active projection and measured render-target viewport. `Frame` uses the projected visual bounds, and `InspectUsdViewport` reports projection, target, yaw, pitch, distance, orthographic scale, image rectangle and window scale factor for screenshot correlation and equal-camera comparisons. For multi-part inspection, wait for `projection_ready: true`, then use the same explicit preview/document handles with `ExplodeUsdPreview { preview, doc_id, assembly, parts, action, axis, spacing }`. `action` is `Enable`, `Update`, or `Reset`; `assembly` must be an authored `kind = "assembly"` path and `parts` must be non-empty exact composed paths below it. Parts are stably ordered by path. Enable captures original local transforms, update reuses that baseline, and reset restores it. The operation is session-scoped presentation state: it never authors USD or changes journal/save/physics state, and reprojection/close clears it. The built-in wrappers are in [`assembly_edit.rhai`](../../assets/scripting/tools/assembly_edit.rhai). The preview helpers are `preview_open`, `preview_view_open`, `preview_view_focus`, `preview_view_mode`, `preview_view_text_layer`, `preview_view_projection`, `preview_view_frame`, `preview_view_reset`, `preview_view_pan`, `preview_view_zoom`, `preview_view_close`, `preview_focus`, `preview_close`, `preview_explode_enable`, `preview_explode_update`, and `preview_explode_reset`. Use `assembly_ui::select_prim(preview, path, extend, toggle)` for selection: it focuses the explicit preview and dispatches `SelectUsdPrim`, which resolves the path only inside that preview's stage and hierarchy. After a selection or screenshot checkpoint, call `assembly_edit::selection_context()` to expose the focused preview's exact `DocumentId`/`UsdPreviewId`/prim-path identities, composed USD type and kind, parent/assembly paths, primary and Inspector-target paths, and the existing typed operation families. Use `selection_context_for(preview)` to inspect a hidden open preview without changing the user's visible focus. The response marks no-selection, multi-selection, stale entries, and duplicate projected paths explicitly. Never use the returned display `name` as an edit key; pass the returned exact path and document/edit target to the existing typed helper. When the existing libraries do not express a reusable policy, create a Twin-scoped or shared library following [`author-rhai-tool`](../author-rhai-tool/SKILL.md). Keep the new function a pure `*_plan` when it authors USD, and use a separate read-only `*_report` or `*_lint` for requirements. Register it with `RegisterToolLibrary`, verify a real namespaced call after the tool-generation maintenance pass, and keep the same headful process for the edit, readback, screenshot, and test. A tool registry listing is not proof that its module is callable. For a document-scoped authoring check, use `cmd("RunLint", #{domain: "usd", doc_id: doc})` only after the document projection is current, then read `query("GetDiagnostics", #{doc_id: doc})`. The response is scoped to that document and reports producer channels with analyzed `generation`, `state`, and `count`; wait for `complete:true` before treating the result as final. An unprojected document remains pending or failed rather than linting a stale stage. The report also warns when one composed entity has duplicate public port owners, including each owner's USD path and registry precedence. Resolve that at the authoring boundary by giving separate semantic owners distinct names; do not hide it with a runtime write retry or fallback. Connections are checked at both authoring and live projection boundaries. `SetConnection` can create the sink attribute, but its source must resolve to the composed source property with the requested type, or to an exact projected runtime port. Invalid source paths, missing prims/properties, type mismatches, missing runtime ports, and invalid runtime direction namespaces fail loudly. `cmd` returns `{id, ok, status, data?, error?}`; poll `command_result(id)` when `status == "pending"` and stop on a terminal `rejected` or `failed` result. The live runtime connection policy is in `assets/scripting/policy/lint_usd.rhai`; Rust supplies only composed/live facts. For semantic component construction, use `assembly_builder::component_bundle_facts` and `assembly_builder::component_bundle_plan` for reusable geometry, collision, mass, dimensions, explicit frame paths, and actuator endpoint contracts. Validate the facts, append the returned `.ops` to one reviewed proposal, then query the composed root and children. The bundle is Rhai policy over existing typed USD operations: it uses `UsdGeom`, `UsdPhysics`, `UsdShade`, `kind`, and `inputs:`/`outputs:` where those standard owners fit. Draft units, limits, and deployment state stay caller-side or in the owning Modelica/joint contract; do not mirror them into unregistered `lunco:` properties. It does not create a material, infer a rigid body/joint, or hide a missing mount relationship. Use the explicit body/joint planners for articulation and `find_compatible_socket`/`mount_component` for an authored socket attachment. Do not hand-author a reference plus guessed transform when a component advertises a mount plug. Mission-specific construction belongs in the owning Twin's Rhai tool library. Keep the core workflow generic: compose a referenced instance, wait for its children, and submit explicit typed operations through the proposal/journal boundary. Treat mass, inertia, dimensions, and frame values as caller-supplied inputs; inspect exact composed prims and proposal diagnostics before committing an authored edit. A Twin recipe may compose these generic plans, but it must not add vehicle-specific builders or writers to the core. For AI-assisted assembly creation, use the generic Rhai `model_authoring` facades as the workflow layer above these builders. Start with `model_context(doc, root, edit_target)`, then run `readiness_report` with the Twin's explicit policy. Use `scene_recipe` for dry placement, terrain, camera, and initial-state ops plus explicit route/program hand-offs. Use `port_graph` and `wiring_plan` for standard USD endpoint discovery and typed connections. Use `publish_component` only after the component contract passes; review/apply its metadata ops and execute its explicit Save-As command. Every result is path- and generation-addressed, and missing data fails visibly. Keep the recipe and policy in the Twin's Rhai/tools package; do not add vehicle-specific logic or a second USD writer to the core. For composed assembly diagnostics, use the companion [`assembly_audit.rhai`](../../assets/scripting/tools/assembly_audit.rhai). Pass the exact document id as the first argument to every stage-reading helper, then the explicit manifest. Use `()` only for a mounted live-scene audit, never to select the focused preview. `QueryUsdPrim` rejects closed, unmapped, and stale document projections; wait for the document projection before auditing it. Standalone previews own their canonical stage through the explicit document's preview lease; they do not need a simulation scene root. Geometry bounds and material reads must follow the ready document generation after an edit. Inspect its structured reports for topology, mount reciprocity, joint frames, rigid-body/joint coverage, and mass/inertia/collider coverage before proposing an edit. Its `explode_plan` only returns preview deltas; it does not write USD or bypass the journal. Do not list raycast wheels as rigid bodies or invent a joint to make that audit pass. Missing relationships, bodies, or colliders are defects to fix at their authored owner. Use `assembly_audit::physicality_report(doc, manifest)` when an asset needs one fail-closed physical/visual-only manifest. A `physical` entry delegates its explicit `mass`, `inertia`, `joints`, and `colliders` to the existing audits. A `visual-only` entry requires a non-empty reason and no composed collision envelope. Duplicate paths, unknown roles, missing prims, and incomplete coverage remain visible structured errors; the tool is dynamically reloadable Rhai policy and does not create a parallel geometry or USD writer. For a direct standard-schema compliance check on a generated or hand-edited component, use `assembly_audit::standard_component_report(doc, manifest)`. Provide the exact root and part paths plus the expected standard `type_name`, shape, physics, visibility, purpose, and material-binding facts. It is read-only, manifest-driven, and reports errors instead of repairing or inferring missing fields; use it before proposing a regeneration or committing an external USDA edit. For `joint_frame_report`, supply `axis` only for revolute, prismatic or spherical joints (their standard omitted axis is X). Fixed payload adapters, distance joints and generic `PhysicsJoint` have no primary axis. Use `require_positions`/`require_rotations` only when the design requires explicit authored frames. Run `assembly_joint_audit` for the diagnostic regression. To inspect and adjust a joint interactively, select the joint prim in the Editor Prims tree and use the Inspector's `USD Joint` `Frames` controls for `Local position 0` and `Local position 1`. Values are canonical metres in the displayed basis. The focused USD preview draws each authored frame as an amber anchor with red/green/blue XYZ arrows and links the two anchors, so a changed position is visible on the composed bodies after reprojection. The markers are preview-only and do not create runtime physics joints; do not move the joint prim's transform when the intent is to change an endpoint frame. Position changes use the existing generation-checked, journalled `ApplyUsdOp` path and remain undoable. Non-finite values are rejected visibly by the Inspector. For numeric part transforms, use the focused preview's Inspector Transform section for referenced children as well as locally authored parts. Wait for the component's dependency closure to load before proposing transforms; a missing composed child must reject, not be manufactured by a preliminary attribute edit. Check inherited transform order and undo with the authored `referenced_part_edit` graphics regression when changing this owner. Use the Inspector Transform section or call the existing `assembly_edit::transform(doc, edit_target, path, translation, rotation, scale, parent_gen)` helper with the exact selection context. Preview values are local canonical metres, Euler XYZ degrees, and unitless scale factors; the Inspector commits changed translation, rotation, and/or scale fields as one journaled `ApplyUsdOps` edit. The typed `UsdOp::SetScale` operation preserves the standard `xformOpOrder` and stage unit/axis boundary. For multiple scalar values on a `kind = "component"` prim, edit the draft Component parameters section and press **Prepare component proposal** once. Review the displayed units, bounds, authored/inherited provenance, and the explicit authoring scope. Invalid composed values remain visible with their diagnostic and block preparation; the Inspector does not clamp them. Commit or reject the matching proposal in the section. Commit uses the same typed USD proposal/journal/projection/undo path as AI/Rhai work and saving remains explicit. Non-component scalar views retain their grouped direct edit surface until their owning schema gets a proposal UI. The native section exposes the standard-property half of the generic `component_editor` contract. A coherent geometry/frame/mass/binding regeneration still requires the explicit Twin-owned bundle and bindings in Rhai; do not infer a recipe from child names. AI callers use `assembly_builder::parameter_plan` or `component_editor::update_plan` and the same proposal flow; do not make one command per field or create a vehicle-specific parameter writer. For a component-level update that must keep geometry, frames, mass, and explicit bindings coherent, use the generic `component_editor` Rhai facade: ```rhai let context = component_editor::selected_update_context(preview, ()); let plan = component_editor::update_plan( context.doc_id, context.edit_target, context.path, bundle, bindings, ); ``` The bundle is an explicit Twin/model-package recipe, not something inferred from a selected prim. The facade delegates to `assembly_builder::component_bundle_update_plan`, so the result is dry and topology-preserving. It updates existing standard `UsdGeom`/`UsdPhysics` values and explicit bindings, while rejecting missing children, kind or role drift, changed material ownership, stale generations, and no-op journals. Review `plan.ops` through `assembly_edit::propose`, `review_session`, and `commit_proposal`. Use `update_context(doc, edit_target, path, requested)` when the path is already known; use `selected_update_context` when starting from the Editor selection. Both preserve the exact document, edit target, path, and generation checkpoint for human and AI workflows. If the edited prim is a live model instance, keep compile-time values as standard authored `inputs:*` overrides on that instance. The USD edit advances the generic `ModelStateRevision`; it never names or invokes a backend. The owning Modelica, Rhai, physics, or future tool adapter decides whether the new state requires recompilation, reset, or ordinary live-input handling. Verify the backend-owned readiness/result after projection and keep the source asset unchanged. When editing a primitive's standard `axis`, compare the visible result after reprojection with the composed attribute. Repeated axis edits must apply the axis correction once to the authored pose, including identity rotation. A generation acknowledgement alone does not prove that the visible pose agrees. The standard transform gizmo is also available for the focused preview: select the exact prim, drag its unparented proxy, and release to commit the changed local translation, Euler XYZ rotation, and/or unitless scale as one generation-checked `ApplyUsdOps` change set. Preview gizmo drags use Bevy's parent-local `GlobalTransform::reparented_to` conversion and never create a live physics identity or hold. Escape cancels; if the preview generation changes or the session closes, the stale transaction is discarded and the newer USD projection is left authoritative. Live simulation entities continue to use the BigSpace gizmo path and `MoveEntity`/`TransformEntity` boundary. Do not send a preview entity through `MoveEntity`: that command owns live BigSpace/physics identities, while the preview owns only the explicit USD document lease. Commit after a coherent value entry so one gesture creates one change set, then inspect the returned generation and screenshot the projected result. The standard gizmo is bound to the presentation owner. A visible focused preview camera receives `GizmoCamera`, and the measured workbench `PanelRects` becomes its logical `GizmoOptions::viewport_rect`. Both the singleton preview and separate preview tabs publish `SceneTarget::Offscreen` through the shared `ScenePickGate`; the maintained gizmo picking backend consumes the same rectangle used by rendering before testing handles. This admits a handle drag despite the global egui focus flag while keeping live-scene input excluded. The editor uses the current occlusion-aware hit map for a gizmo proxy on the press frame instead of waiting for the gizmo focus flag that updates later in the frame. It latches that primary gesture through release or cancellation; preview pan stays suppressed even after the pointer leaves the image. The gizmo hit layer is above the preview capture layer, preserving handle hover without allowing the preview to leak input to the live scene. Do not add panel-specific cursor math or a second gizmo driver. For editor mouse automation, use the existing `input_pointer_*` Rhai helpers and derive logical coordinates from `InspectUsdViewport`'s measured `image_rect` and `scale_factor`. The production regression is in `assets/scenarios/tests/assembly_property_editor.rhai` and checks both a gizmo drag and a blank-area camera pan. The presentation helpers are in [`assembly_ui.rhai`](../../assets/scripting/tools/assembly_ui.rhai): use `panel_templates(preview, doc, edit_target)` to discover the nine existing Editor surfaces/workflows and their explicit handles. `open_session(preview, doc, edit_target)` activates the Editor, opens/focuses the explicit preview, and foregrounds its primary view tab. Use `focus`, `open_structure`, `open_inspector`, `open_connections`, `open_animation`, `open_mount`, and `open_review` to focus the owning registered panels. Animation is an Environment section and mount and review are Inspector sections; persistence is the existing document lifecycle command group and has no fabricated panel. These helpers only dispatch existing `ActivatePerspective`, preview-focus, and `FocusPanel` commands; they do not own layout or create parallel document/view state. Discover reflected command shapes with `DiscoverSchema` rather than inventing JSON for a new command. ## Use the generic authoring evidence tools For reviewable model-authoring feedback, use the registered Rhai `authoring_inspection` library rather than adding an assembly-specific panel, diagnostic schema, or Rust policy. Its read/presentation functions are: - `authoring_inspection::candidate_diff(before_doc, after_doc, paths)` compares only the exact affected USD paths from the proposal or edit session. It reports field changes, material/collision/joint/frame/port/topology consequences, and the document-scoped lint state. Do not compare complete USDA strings or infer changes from screenshots. - `authoring_inspection::group_diagnostics(findings)` groups the existing findings by owner and severity while retaining every raw finding and subject. `authoring_inspection::navigate_diagnostic(preview, view, finding)` selects the finding's absolute `subject` through the canonical preview selection owner and frames that exact path with `FrameUsdPreviewSelection`. It never searches the live scene by display name. - `authoring_inspection::inspection_snapshot(doc, root, path)` returns one read-only record containing visual, collision, joint, frame, material, provenance, schema, and connection evidence. Missing collision or provenance is represented explicitly with `present: false` and a reason; do not fill it with guessed geometry or metadata. - `authoring_inspection::inspection_mode(doc, root, path, enabled)` composes that snapshot with the generic `SetDiagnosticLayers` presentation command. The mode is view-only and its layers are runtime diagnostics, not authored USD facts or a second scene graph. Persist a view-only camera through the typed `assembly_edit` wrappers `save_inspection_preset`, `apply_inspection_preset`, `delete_inspection_preset`, and `inspection_presets`. Presets belong to the shared settings boundary, not the USD document or journal. Use the exact `UsdPreviewViewId`, and verify `InspectUsdViewport.active_preset`; the query `InspectUsdInspectionPresets` is the authoritative persisted list. Saving or applying a preset must not author a camera prim, change selection, or replace the preview lease. Keep the workflow bounded by the existing preset limit and surface rejected names or missing views as errors. The same document id, preview lease, view id, and projection generation must flow through every read and presentation command. Wait for `InspectUsdViewport.projection_ready` before querying or framing a path, and rerun the evidence read after an authored change. The production `selection_ai_workflow` scenario is the regression surface for positive and negative candidate diffs, diagnostic navigation, inspection snapshots, mode toggles, and preset save/apply/delete behavior. ## Choose the USD ownership scope Resolve the target before authoring. Select the smallest scope that owns the fact: | Scope | Author here | |---|---| | `SourceAsset` | reusable lander/component geometry, ports, mount plug, or physical contract | | `Assembly` | Twin-owned wrapper, component references, sockets, joints, and assembly composition | | `InstanceOverride` | one composed mission instance, only when the existing USD arc permits the override | For a referenced or payloaded prim, a composed read may be read-only at the current layer. Use the explicit `edit_scope` from `ResolveUsdTarget`; fork an open document with `ForkDocument` when an independent editable assembly is intended, then use `SaveAsDocument` to give it a file identity. Never write a silent override into a different layer or edit the source file behind the editor's back. ## Make an edit through the existing tools Use the smallest existing typed intent that expresses the change: - `transform`, `attribute`, `schema`, `variant`, `relationship`, and `connection` lower to `ApplyUsdOp`/`ApplyUsdOps`. - Dynamic shader parameters are authored on the bound USD `Shader` prim as `inputs:`. Resolve the composed Shader and exact declared `typeName` first; preserve USD roles and array shape, validate the literal with the shared USD parser, and submit one grouped edit for a multi-field change. The same resolver is used by `SetObjectProperty`; never guess a geometry `primvars:` destination from a parameter name. - `add_prim`, `remove_prim`, `move_prim`, `payload`, and `active` expose the existing typed structural USD operations. They require an explicit target, exact paths, and the inspected generation; they never replace a layer's raw source. - Source-defined geometry tools follow the same explicit contract. For example, `nurbs::set_points(doc, path, points, parent_gen)` returns the `ApplyUsdOp` result; pass the `DocumentId` and generation from `describe`/`QueryUsdPrim`, and inspect `ok`, `data.doc_id`, `data.paths`, `data.generation`, and `data.change_set_id` before treating the edit as accepted. Use `nurbs::set_point(doc, path, index, point)` only when the boolean convenience result is sufficient. Never infer the document from the active editor tab. - Every newly authored component must declare `upAxis = "Y"` and `metersPerUnit = 1.0` in its own stage header. `NewDocument` supplies this canonical scaffold. If an imported stage reports different metrics, stop, record the stage convention, and verify the component in its own preview before placing it in an assembly; never compensate with unexplained scale or translation constants. - `assembly_edit::references(doc, edit_target, path, references, list_op, parent_gen)` authors existing reference arcs through `SetReferenceArcs`. Each entry carries an asset identity and optional absolute target prim path. `Prepend`, `Append`, `Add`, and `Delete` preserve weaker-layer opinions; `Explicit` replaces the selected layer's list and an empty explicit list clears it. Inspect `references.authored` and `references.composed` before editing, and resolve the target first so a composed-only prim is reported as read-only rather than flattened or guessed. - `assembly_edit::default_prim(doc, edit_target, path, parent_gen)` authors the stage root `defaultPrim` in `@root@` or `@runtime@`; use an absolute or root-relative existing prim path, and pass `()` to clear only that layer. `assembly_edit::prim_kind(doc, edit_target, path, kind, parent_gen)` authors a USD identifier such as `component`, `assembly`, or `group` on an existing prim; pass `()` to clear that layer's opinion. Inspect `metadata.defaultPrim` and `prim.metadata.kind` for root/runtime, document-composed, and canonical-stage values with their source labels. - `assembly_edit::stage_metrics(doc, edit_target, meters_per_unit, up_axis, parent_gen)` authors the shared typed `StageMetrics`/`UpAxis` values at the stage root. Use the exact source convention from inspection or its owning requirement; the scale must be finite and positive. This is a document-wide coordinate interpretation change and triggers a whole-stage projection rebuild. Correct a missing/wrong stage unit here instead of compensating with per-component scale or translation edits. - `assembly_edit::attach_component` and `assembly_edit::detach_component` use the existing mount, socket, joint, frame, ownership, and occupancy validators. Supply exact paths in the reflected `AttachSpec`/`DetachSpec`; never identify a part by a name prefix such as `Wheel_`. - For an existing socket attachment, use `assembly_builder::mount_frame_realignment_plan` with the exact host, socket, part, and recorded joint paths. It composes nested canonical rigid `translate`/`rotateXYZ` frame chains in the dynamically reloadable Rhai tool, emits one reviewed part-transform plus joint-anchor plan, and preserves topology. It rejects missing or ambiguous mount relationships, unsupported frame operations, non-unit scale, malformed values, and body mismatches before proposal; do not hand-copy the socket pose into the part or joint. - Before choosing an assembly action, use `assembly_builder::authoring_context(doc, path, edit_target)`. It returns the exact identity, generation, resolved authored target, topology/collision facts, socket occupancy, plug relationships, and only the actions supported by those authored facts. For a dry AI or human intent, use `assembly_builder::place_or_attach_plan` with either `mode: "attach_component"` (review `.spec`, then call the existing `assembly_edit::attach_component`) or `mode: "realign_existing_mount"` (review `.ops`, then use the normal proposal flow). Never add an AI-only writer or guess a frame from a part name. - When the user is already working in the Editor, use `assembly_builder::selected_authoring_context(preview)` to bridge the exact single selection into the same authoring record. Pass `()` for the focused preview or an explicit preview id for a hidden session. It rejects no, multiple, stale, and ambiguous selection state; preserve its selection identity and generation until proposal review. - Use `assembly_builder::functional_frame_catalog(doc, edit_target, root_path)` to read the registered mount frame. It follows only the authored `lunco:mount:frame` relationship and returns its exact path, standard transform facts, and socket paths. Keep generic datum and actuator paths as explicit plan inputs consumed by standard joint or Modelica contracts. Use `assembly_builder::align_frames_plan(doc, edit_target, moving_path, moving_frame_path, target_path, target_frame_path)` for a dry two-op visual placement plan; the roots must be sibling Xforms and the frame stacks must be rigid `translate`/`rotateXYZ` with unit scale. Physical mount topology still goes through the attach/realignment planners above. - `assembly_edit::attach_program(doc, spec)` dispatches the existing typed `AttachProgram` contract. Build its `inputs` and `outputs` with the namespaced helpers `assembly_edit::program_input_connection`, `assembly_edit::program_input_default`, and `assembly_edit::program_output`; the source asset, host path, program name, edit target, and port paths remain explicit in `spec`. - `assembly_edit::rigid_body_plan(edit_target, parent_path, name, mass, center_of_mass, diagonal_inertia)` returns a typed operation plan for a new body frame with explicit `PhysicsRigidBodyAPI`, `PhysicsMassAPI`, mass, centre-of-mass, and diagonal inertia. `assembly_edit::revolute_joint_plan` similarly requires two body paths, both local anchors and quaternions, a cardinal axis, ordered degree limits, and collision policy. Append the returned `.ops` to one reviewed proposal; these helpers do not author transforms, shapes, or hidden defaults. - Use `assembly_edit::fixed_joint_plan` for a rigid adapter. It requires two distinct absolute body paths, both local anchors and quaternions, and an explicit collision policy, returning the standard `PhysicsFixedJoint` operations for the same reviewed proposal boundary. - For construction from parts, use the hot-reloadable `assembly_builder` library. `place_plan` emits a local transform plan; `frame_plan` emits a validated Xform frame; `cube_plan`, `cylinder_shape_plan`, and `movable_cube_plan` compose standard geometry, collider, body, mass, and placement facts; `existing_rigid_body_plan` promotes an exact referenced Xform by defining one local over while retaining its authored geometry; it rejects a repeat once a complete body contract is already authored; `hinge_plan` delegates to the framed revolute planner; and `align_centers_plan`/`align_cube_edges_plan` derive placements from explicit queried paths and reject unsupported parent/frame assumptions. These helpers return the same typed `.ops` consumed by `propose`/`batch`; completed body/joint identities are rejected by the construction recipe and require an explicit update plan; these helpers do not write USDA or bypass the document owner. - For a reusable referenced assembly, use `assembly_builder::referenced_instance_plan` or `assembly_builder::referenced_instance_targeted_plan` when the source prim must be explicit, or `assembly_builder::referenced_instance_plan` (or its targeted form) to author the explicit component identity, asset URI, parent, and local pose. Materialize first-use references before applying composition-changing variants; once the composed children are queryable, use `assembly_builder::select_variants_plan` as a second reviewed plan. Parameter changes use `assembly_builder::parameter_plan` and the same typed `ApplyUsdOps` command path as Inspector edits. This sequencing keeps async reference loading and coarse variant recomposition from producing a root with missing children. - For generic human or AI property editing, start with `assembly_builder::editable_property_catalog(doc, path, edit_target, requested)`. Pass an explicit field array for a focused view or `()` to discover supported standard `UsdGeom`, `UsdPhysics`, `UsdShade`, `kind`, variant, and `inputs:`/`outputs:` fields. The result includes the USD owner, exact type, units, composed value, USDA literal, authored/editable status, edit scope, and source path. `xformOpOrder` and `extent` are visible but read-only; unknown names and guessed `lunco:` fields fail visibly. - Build a dry change set with `assembly_builder::editable_property_patch_plan(doc, edit_target, path, edits, parent_gen)`, where each edit is `{ name, value, type_name }`. It reuses the existing typed transform, attribute, relationship, kind, and variant operations, checks the exact generation and target scope, rejects wrong types/paths and structural edits, and reports a true `no_op` with an empty `.ops` list when values already match. Submit `.ops` through the normal `assembly_edit::propose`/`review_session`/`commit_proposal` flow; the planner never writes USDA directly. `InspectUsdDocument` exposes standard variant selections at `prim.metadata.variantSelections` for the same human/AI read path. - For repeated references, use `assembly_builder::referenced_instance_pattern_plan` with one explicit template and an ordered array of `{ name, translation, rotation, scale }` placements. It preserves that order, composes the existing referenced instance planner, and rejects duplicate names or placements without a local pose. For a reflected placement, use `assembly_builder::referenced_instance_mirror_plan`; provide a cardinal local axis and source translation. It rejects non-zero source Euler rotation because the reflected orientation must be authored explicitly, rather than silently guessed. Review the returned `.ops` before committing. - Use `assembly_builder::place_with_clearance_plan` when placing a part near other authored geometry. Supply the exact moving frame and Cube shape plus every exact blocker frame and Cube shape. The frames must share a translation-only authored parent; the helper rejects rotated frames, non-Cube geometry, duplicate blockers, overlap, and less than the requested gap before proposal. The returned transform remains a normal reviewed change set. - Use `assembly_builder::place_with_collision_clearance_plan` for referenced, Cylinder, Mesh, or compound bodies. Supply exact moving/blocker body paths and a minimum gap; it reads `QueryUsdPrim { collision_bounds: true }` from the shared composed collision owner, requires a translation-only common parent chain, and rejects missing, malformed, unsupported, overlapping, or duplicated collision envelopes before proposal. This is the preferred dynamic Rhai path for general assembly placement; it does not duplicate primitive dimensions or write USD directly. - For one selected prim's complete read-only visual/physics explanation, use `QueryUsdPrim { topology: true }`. The result scopes to the nearest `PhysicsRigidBodyAPI` ancestor (or the selected prim), then returns one `topology.parts` list with visual/collider flags, inherited purpose, collision state, body owner, per-shape canonical bounds, local/world frames, source-layer head, and render/physics material plus resolved shader paths. `topology.joints` reports standard joint body targets in the same scope; `topology.diagnostics` is authoritative for malformed or unavailable facts. `topology.projection` and `topology.binding` expose composed generation and the existing USD projection markers. This query is opt-in, read-only, and does not replace selection or create a second scene graph. - For general-body snap editing, use `assembly_builder::align_collision_centers_plan` or `assembly_builder::align_collision_edges_plan`. They align aggregate composed bounds for exact sibling paths on an explicit axis, edge, and gap, preserve unrelated translation components, and return reviewed `SetTranslate` plans. The same translation-only parent-chain and malformed/missing collision-data checks apply. - `batch` or a proposal is one journal/change-set unit when an intent changes multiple facts. Supply the inspected `parent_gen` so a stale edit fails atomically. - `propose` → `review_session` / `review_proposal` → `commit_proposal` is the interactive review flow for a multi-operation change. Proposal creation and review do not mutate the document; commit enters the ordinary USD journal and undo path. A conflict requires a fresh inspection and proposal. For a new assembly, use `NewDocument` or `ForkDocument`, add existing assets by USD reference through the typed operation/attachment surface, and author standard `UsdGeom`, `UsdPhysics`, `UsdShade`, and `UsdLux` fields. Do not make a lander-specific command, a second resolver, a direct USDA writer, or a runtime-only ECS setter. Every actual asset edit must happen while the headful session is visible and through the command/API or Editor panel. Do not use `sed`, a generated USDA replacement, a direct filesystem write, or direct ECS mutation to get around a rejected command. A rejected target or invalid topology is feedback from the authoritative owner and must be fixed there. ## Concrete assembly workflow Open the target Twin, inspect the composed assembly, and decide whether the requested change belongs in the Twin wrapper or in the reusable source asset. For a new payload, inspect the host's composed mount sockets and the component's plug frame first, then submit one validated `AttachComponent` intent. The lowering authors the reference, placement, joint, relationships, and socket occupancy together. For a change to an existing mounted part, submit its exact component, joint, and optional socket paths to `DetachComponent` or use the typed transform/attribute operation at the resolved layer; do not re-create the component merely to change its pose. After each change, visually check the lander's geometry, material, pose, joint attachment, and collision relationship in the focused preview. A visually plausible result is not enough: query the composed paths and verify that the expected `UsdPhysics` bodies/joints and authored relationships exist. For a repeatable lander workflow, use the returned document identity and generation rather than a path-derived guess: ```rhai let before = assembly_edit::describe(lander_doc); let target = assembly_edit::resolve_target(lander_doc, lander_path, "@root@"); assembly_ui::open_session(lander_preview, lander_doc, "@root@"); let proposal = assembly_edit::propose( lander_doc, "Assembly", "Adjust lander pose", inspected_typed_ops, before.generation, ); ``` Here `inspected_typed_ops` is the exact `UsdOp` plan built from composed inspection. Review the proposal in the existing Inspector, commit it after the visible checkpoint, then inspect the affected paths with the new generation. For a mount, submit the exact reflected `AttachSpec` obtained from `DiscoverSchema` to `assembly_edit::attach_component`; it must carry the component, socket, plug, joint, frame, and ownership paths from the composed inspection. For a multi-asset workflow, keep the documents and preview leases independent: ```rhai let lander_before = assembly_edit::describe(lander_doc); let payload_before = assembly_edit::describe(payload_doc); assembly_ui::open_session(lander_preview, lander_doc, "@root@"); assembly_ui::open_session(payload_preview, payload_doc, "@root@"); let ack = assembly_edit::batch( lander_doc, "Place inspected payload components", [ #{ SetTranslate: #{ edit_target: "@root@", path: lander_path, value: lander_translation } }, #{ SetTranslate: #{ edit_target: "@root@", path: second_lander_path, value: second_lander_translation } }, ], lander_before.generation, ); let payload_unchanged = assembly_edit::sync_document(payload_doc, payload_before.generation); ``` Use the reflected `AttachSpec` with `assembly_edit::attach_component` when the assets must become one mounted assembly; do not merge source documents by copying layers. The translated paths are inspected paths in `lander_doc`, while `payload_before` remains the payload document's independent revision cursor. A stale generation is a rejected edit and requires a fresh inspection before retrying. ## Save, verify, and close Do not save automatically as part of proposal commit. The optional `editor_workflow::after_edit(doc)` helper can coordinate the explicit inspect → current projection → document lint checkpoint after a reviewed apply/commit. It returns a retryable projection result and never saves that stale generation. Authored autosave is disabled by default: an omitted `usd.editor_autosave` Twin setting or `false` returns `save_required`; only an explicit `true` permits the helper to call `SaveDocument`. After the user approves the visible result, call `assembly_edit::save_document(doc)` for a file-backed document or `assembly_edit::save_as_document(doc, path)` for a fork, then confirm the document is no longer dirty with `ListOpenDocuments`/`InspectUsdDocument`. Use `assembly_edit::discard_document(doc)` to restore the file through the owner, or `assembly_edit::close_document(doc)` after the final checkpoint. Keep undo/redo available through `UndoDocument` and `RedoDocument` during feedback. For runtime behavior, use the production scene/scenario gate and inspect its real verdict. An Editor preview must not synthesize or execute Modelica merely because it contains a component collection; use `preview_domain_isolation` when changing that runtime admission boundary. Preview queries remain valid even when the authored network is incomplete. For physical acceptance, inspect the production scene/scenario's real verdict. `--validate` is parse/preflight only. For code or authored asset changes, run the narrowest relevant checks after the interactive session; do not claim a screenshot proves physics, persistence, or reload. Before closing, capture the final screenshot and typed state, then use `CloseUsdPreview` and the API `Exit` only for sessions owned by this agent. Verify the process and port are gone. ## Non-negotiable boundaries - USD owns identity, topology, references, frames, parameters, materials, and physics facts; ECS is only its projection. - A hierarchy is not an attachment. A movable mounted rigid body needs its authored `UsdPhysics` joint and frames. - `UsdPreviewId` owns transient session selection and panel state while each `UsdPreviewViewId` owns one presentation camera/render target; neither is authored into USD. - Use standard USD schemas whenever they own the concept, and existing `lunco-usd-compose`, journal, mount, and transform-frame owners whenever they already implement it. - No name-based discovery, compatibility alias, legacy path, fallback layer, direct file mutation, or second history/state mechanism.