From c5ca6a7501a07c744406dc0729ac40b0a3b945ef Mon Sep 17 00:00:00 2001 From: indigo Date: Mon, 6 Jul 2026 03:16:58 +0800 Subject: [PATCH] Show composition arcs and asset info in PropertyPanel Prims with authored references or payloads get a section below the prim header listing every list-op entry (explicit/add/prepend/append/ delete) with its asset path and target prim; a folder icon button browses (USD/usdz/MaterialX/Alembic filter) and replaces the entry's asset path, authored as one undoable whole-list-op swap into the edit target. Prims with composed assetInfo get a read-only key/value section (resolved paths shown in tooltips). PropertyPanel now receives the IconManager for the browse button. Co-Authored-By: Claude Fable 5 --- src/ui/Application.cpp | 1 + src/ui/PropertyPanel.cpp | 200 ++++++++++++++++++++++++++++++++++++++- src/ui/PropertyPanel.h | 10 ++ 3 files changed, 210 insertions(+), 1 deletion(-) diff --git a/src/ui/Application.cpp b/src/ui/Application.cpp index a55deac..dd87dae 100644 --- a/src/ui/Application.cpp +++ b/src/ui/Application.cpp @@ -111,6 +111,7 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig m_iconManager = std::make_unique(); m_iconManager->Initialize(ResourcePath("resources/icons"), 24); m_sceneHierarchyPanel->SetIconManager(m_iconManager.get()); + m_propertyPanel->SetIconManager(m_iconManager.get()); m_viewportPanel->SetIconManager(m_iconManager.get()); m_timelinePanel->SetIconManager(m_iconManager.get()); m_stageEditorPanel->SetIconManager(m_iconManager.get()); diff --git a/src/ui/PropertyPanel.cpp b/src/ui/PropertyPanel.cpp index df978eb..897fbe8 100644 --- a/src/ui/PropertyPanel.cpp +++ b/src/ui/PropertyPanel.cpp @@ -1,8 +1,16 @@ #include "PropertyPanel.h" #include "../utils/Logger.h" +#include "../utils/FileDialog.h" #include "../core/commands/TransformCommand.h" #include "../core/commands/AttributeSetCommand.h" +#include +#include +#include +#include +#include +#include + #include #include #include @@ -1116,6 +1124,186 @@ void PropertyPanel::RenderPrimHeader(const pxr::UsdPrim& prim) { ImGui::EndTable(); } +// --------------------------------------------------------------------------- +// References & Payloads – one row per list-op entry; clicking the asset path +// browses for a replacement file (undoable whole-list-op swap). +// --------------------------------------------------------------------------- +namespace { + +pxr::SdfReference WithAssetPath(const pxr::SdfReference& r, const std::string& assetPath) { + return pxr::SdfReference(assetPath, r.GetPrimPath(), r.GetLayerOffset(), r.GetCustomData()); +} +pxr::SdfPayload WithAssetPath(const pxr::SdfPayload& p, const std::string& assetPath) { + return pxr::SdfPayload(assetPath, p.GetPrimPath(), p.GetLayerOffset()); +} + +// Shared row renderer for SdfReferenceListOp / SdfPayloadListOp: one row per +// item in each list-op bucket, labelled with its op. On replace, the whole +// modified list op is authored to the edit target as one undoable command. +template +void RenderArcListRows(const pxr::UsdPrim& prim, CommandHistory* history, + IconManager* icons, + const char* tag, const char* displayKind, + const pxr::TfToken& fieldKey, const ListOpT& listOp, int& uid) +{ + using ItemVector = typename ListOpT::ItemVector; + struct Bucket { const char* opName; ItemVector items; }; + Bucket buckets[] = { + {"explicit", listOp.GetExplicitItems()}, + {"add", listOp.GetAddedItems()}, + {"prepend", listOp.GetPrependedItems()}, + {"append", listOp.GetAppendedItems()}, + {"delete", listOp.GetDeletedItems()}, + }; + const float rowH = ImGui::GetFrameHeight(); + + for (size_t b = 0; b < sizeof(buckets) / sizeof(buckets[0]); ++b) { + for (size_t i = 0; i < buckets[b].items.size(); ++i) { + const auto& item = buckets[b].items[i]; + std::string display = item.GetAssetPath(); + if (!item.GetPrimPath().IsEmpty()) + display += " <" + item.GetPrimPath().GetString() + ">"; + + ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH); + ImGui::PushID(uid++); + + ImGui::TableSetColumnIndex(0); + DrawPropertyMiniButton(tag, /*authored=*/true, + item.GetAssetPath().c_str(), displayKind); + + ImGui::TableSetColumnIndex(1); + ImGui::AlignTextToFramePadding(); + ImGui::TextDisabled("%s", buckets[b].opName); + + ImGui::TableSetColumnIndex(2); + // Browse icon button first, then the path as plain text. + bool clicked; + if (icons) { + clicked = ImGui::ImageButton("##browseArc", + ImTextureRef(icons->Get(Icon::FolderOpen)), + ImVec2(14.f, 14.f)); + } else { + clicked = ImGui::SmallButton("..."); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Browse for a replacement file"); + ImGui::SameLine(); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(display.empty() ? "(no asset)" : display.c_str()); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s (%s)\n%s", displayKind, buckets[b].opName, + item.GetAssetPath().c_str()); + if (clicked) { + // Anything USD can compose is a valid arc target: native USD + // layers plus file formats with layer plugins (MaterialX, + // Alembic). + static const char* kArcFileFilter = + "Referenceable (*.usd;*.usda;*.usdc;*.usdz;*.mtlx;*.abc)\0" + "*.usd;*.usda;*.usdc;*.usdz;*.mtlx;*.abc\0" + "USD Files (*.usd;*.usda;*.usdc;*.usdz)\0*.usd;*.usda;*.usdc;*.usdz\0" + "MaterialX (*.mtlx)\0*.mtlx\0" + "Alembic (*.abc)\0*.abc\0" + "All Files (*.*)\0*.*\0"; + std::string picked = FileDialog::OpenFile(kArcFileFilter, "Select Reference File"); + if (!picked.empty() && picked != item.GetAssetPath()) { + ListOpT newOp = listOp; + ItemVector items = buckets[b].items; + items[i] = WithAssetPath(item, picked); + switch (b) { + case 0: newOp.SetExplicitItems(items); break; + case 1: newOp.SetAddedItems(items); break; + case 2: newOp.SetPrependedItems(items); break; + case 3: newOp.SetAppendedItems(items); break; + case 4: newOp.SetDeletedItems(items); break; + } + // Authors the whole (possibly multi-layer-composed) list + // op into the edit target — the pragmatic single-layer + // behaviour; undo restores the previous composed op. + if (history) { + pxr::UsdPrim p = prim; + ListOpT oldOp = listOp; + history->Push(std::make_unique( + std::string("Replace ") + displayKind + " Path", + [p, fieldKey, newOp]() mutable { p.SetMetadata(fieldKey, newOp); }, + [p, fieldKey, oldOp]() mutable { p.SetMetadata(fieldKey, oldOp); })); + } else { + pxr::UsdPrim(prim).SetMetadata(fieldKey, newOp); + } + } + } + ImGui::PopID(); + } + } +} + +} // namespace + +void PropertyPanel::RenderCompositionArcsSection(const pxr::UsdPrim& prim) { + if (!ImGui::CollapsingHeader("References & Payloads", ImGuiTreeNodeFlags_DefaultOpen)) + return; + + constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg; + if (!ImGui::BeginTable("##compArcs", 3, kFlags)) return; + ImGui::TableSetupColumn("##dot", ImGuiTableColumnFlags_WidthFixed, 20.f); + ImGui::TableSetupColumn("##op", ImGuiTableColumnFlags_WidthFixed, 60.f); + ImGui::TableSetupColumn("##val", ImGuiTableColumnFlags_WidthStretch); + + int uid = 0; + if (prim.HasAuthoredReferences()) { + pxr::SdfReferenceListOp refOp; + if (prim.GetMetadata(pxr::SdfFieldKeys->References, &refOp)) + RenderArcListRows(prim, m_commandHistory, m_iconManager, "(R)", "Reference", + pxr::SdfFieldKeys->References, refOp, uid); + } + if (prim.HasAuthoredPayloads()) { + pxr::SdfPayloadListOp payloadOp; + if (prim.GetMetadata(pxr::SdfFieldKeys->Payload, &payloadOp)) + RenderArcListRows(prim, m_commandHistory, m_iconManager, "(P)", "Payload", + pxr::SdfFieldKeys->Payload, payloadOp, uid); + } + ImGui::EndTable(); +} + +// --------------------------------------------------------------------------- +// Asset Info – read-only view of the composed assetInfo dictionary +// --------------------------------------------------------------------------- +void PropertyPanel::RenderAssetInfoSection(const pxr::UsdPrim& prim) { + if (!ImGui::CollapsingHeader("Asset Info", ImGuiTreeNodeFlags_DefaultOpen)) + return; + + constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg; + if (!ImGui::BeginTable("##assetInfo", 2, kFlags)) return; + ImGui::TableSetupColumn("##key", ImGuiTableColumnFlags_WidthFixed, 110.f); + ImGui::TableSetupColumn("##val", ImGuiTableColumnFlags_WidthStretch); + + const pxr::VtDictionary info = prim.GetAssetInfo(); + std::vector keys; + keys.reserve(info.size()); + for (const auto& entry : info) + keys.push_back(entry.first); + std::sort(keys.begin(), keys.end()); + + for (const std::string& key : keys) { + const pxr::VtValue& value = info.find(key)->second; + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextDisabled("%s", key.c_str()); + ImGui::TableSetColumnIndex(1); + if (value.IsHolding()) { + const auto& assetPath = value.UncheckedGet(); + ImGui::TextUnformatted(assetPath.GetAssetPath().c_str()); + if (ImGui::IsItemHovered() && !assetPath.GetResolvedPath().empty()) + ImGui::SetTooltip("resolved: %s", assetPath.GetResolvedPath().c_str()); + } else { + const std::string text = pxr::TfStringify(value); + ImGui::TextUnformatted(text.c_str()); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", text.c_str()); + } + } + ImGui::EndTable(); +} + // --------------------------------------------------------------------------- // Variant Sets – no borders, no header (mirrors DrawVariantSetsCombos in usdtweak) // --------------------------------------------------------------------------- @@ -1605,9 +1793,19 @@ void PropertyPanel::Render() { ImGui::Separator(); - // Scrollable body – variant sets → transform → all properties + // Scrollable body – composition arcs → variant sets → transform → all properties ImGui::BeginChild("##body"); + if (prim.HasAuthoredReferences() || prim.HasAuthoredPayloads()) { + RenderCompositionArcsSection(prim); + ImGui::Separator(); + } + + if (!prim.GetAssetInfo().empty()) { + RenderAssetInfoSection(prim); + ImGui::Separator(); + } + if (prim.HasVariantSets()) { RenderVariantSetsSection(prim); ImGui::Separator(); diff --git a/src/ui/PropertyPanel.h b/src/ui/PropertyPanel.h index 8a93d01..288d51c 100644 --- a/src/ui/PropertyPanel.h +++ b/src/ui/PropertyPanel.h @@ -3,6 +3,7 @@ #include "../core/PropertyManager.h" #include "../core/CommandHistory.h" #include "../core/commands/TransformCommand.h" +#include "IconManager.h" #include #include #include @@ -25,6 +26,7 @@ public: void SetPropertyManager(PropertyManager* manager); void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; } + void SetIconManager(IconManager* icons) { m_iconManager = icons; } void SetStage(pxr::UsdStageRefPtr stage); void SetSelectedPrimPath(const std::string& path); @@ -60,12 +62,20 @@ private: void RenderTransformSection(); void RenderPropertiesTable(const pxr::UsdPrim& prim); ///< groups attrs by API schema void RenderMaterialBindSection(const pxr::UsdPrim& prim); ///< material:binding info + /// References/payloads authored on the prim, one row per list-op entry + /// (explicit/add/prepend/append/delete) with its asset path; clicking a + /// path browses for a replacement file (undoable). + void RenderCompositionArcsSection(const pxr::UsdPrim& prim); + /// Read-only view of the prim's composed assetInfo dictionary + /// (identifier, name, version, ...). + void RenderAssetInfoSection(const pxr::UsdPrim& prim); void RenderAttrRelSubTable(const std::vector& attrs, const std::vector& rels, const pxr::UsdEditTarget& editTarget); ///< inner 3-col table PropertyManager* m_propertyManager = nullptr; CommandHistory* m_commandHistory = nullptr; + IconManager* m_iconManager = nullptr; pxr::UsdStageRefPtr m_stage; std::string m_selectedPrimPath;