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 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 03:16:58 +08:00
parent de94decf64
commit c5ca6a7501
3 changed files with 210 additions and 1 deletions
+1
View File
@@ -111,6 +111,7 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
m_iconManager = std::make_unique<IconManager>();
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());
+199 -1
View File
@@ -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 <pxr/usd/sdf/listOp.h>
#include <pxr/usd/sdf/reference.h>
#include <pxr/usd/sdf/payload.h>
#include <pxr/usd/sdf/schema.h>
#include <pxr/base/vt/dictionary.h>
#include <pxr/base/tf/stringUtils.h>
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/attribute.h>
#include <pxr/usd/usd/relationship.h>
@@ -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 <typename ListOpT>
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<AttributeSetCommand>(
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<std::string> 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<pxr::SdfAssetPath>()) {
const auto& assetPath = value.UncheckedGet<pxr::SdfAssetPath>();
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();
+10
View File
@@ -3,6 +3,7 @@
#include "../core/PropertyManager.h"
#include "../core/CommandHistory.h"
#include "../core/commands/TransformCommand.h"
#include "IconManager.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/usd/timeCode.h>
#include <pxr/usd/sdf/path.h>
@@ -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<pxr::UsdAttribute>& attrs,
const std::vector<pxr::UsdRelationship>& 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;