diff --git a/src/ui/PropertyPanel.cpp b/src/ui/PropertyPanel.cpp index eb4e5ed..5448a70 100644 --- a/src/ui/PropertyPanel.cpp +++ b/src/ui/PropertyPanel.cpp @@ -1,6 +1,7 @@ #include "PropertyPanel.h" #include "../utils/Logger.h" #include "../core/commands/TransformCommand.h" +#include "../core/commands/AttributeSetCommand.h" #include #include @@ -38,8 +39,15 @@ #include #include #include +#include +#include +#include +#include #include #include +#include +#include +#include PXR_NAMESPACE_USING_DIRECTIVE @@ -48,10 +56,12 @@ namespace UsdLayerManager { // --------------------------------------------------------------------------- // Colours // --------------------------------------------------------------------------- -static const ImVec4 kColorX { 0.86f, 0.30f, 0.30f, 1.f }; // X axis – red -static const ImVec4 kColorY { 0.40f, 0.80f, 0.30f, 1.f }; // Y axis – green -static const ImVec4 kColorZ { 0.28f, 0.56f, 0.96f, 1.f }; // Z axis – blue -static const ImVec4 kColorLabel{ 0.80f, 0.80f, 0.80f, 1.f }; // row labels +static const ImVec4 kColorX { 0.86f, 0.30f, 0.30f, 1.f }; // X axis – red +static const ImVec4 kColorY { 0.40f, 0.80f, 0.30f, 1.f }; // Y axis – green +static const ImVec4 kColorZ { 0.28f, 0.56f, 0.96f, 1.f }; // Z axis – blue +static const ImVec4 kColorLabel { 0.80f, 0.80f, 0.80f, 1.f }; // row labels +static const ImVec4 kColorKeyOn { 1.00f, 0.65f, 0.10f, 1.f }; // key exists at editTime +static const ImVec4 kColorKeyOff { 0.35f, 0.35f, 0.35f, 1.f }; // varying, no sample here // --------------------------------------------------------------------------- PropertyPanel::PropertyPanel() = default; @@ -535,6 +545,60 @@ static std::string GetPropDisplayName(const pxr::UsdProperty& prop) { return ns.empty() ? bn : (ns + ":" + bn); } +// --------------------------------------------------------------------------- +// Helper: check whether an attribute has an exact time sample at t +// --------------------------------------------------------------------------- +static bool HasExactTimeSample(const pxr::UsdAttribute& attr, double t) { + double lo = 0.0, hi = 0.0; + bool has = false; + attr.GetBracketingTimeSamples(t, &lo, &hi, &has); + return has && (lo == t) && (hi == t); +} + +// --------------------------------------------------------------------------- +// Helper: small "(k)" key button for animatable (Varying) attributes. +// Orange = exact time sample at editTime; dim gray = no sample at this frame. +// Click = Set(currentValue, editTime) with undo. +// --------------------------------------------------------------------------- +static void DrawKeyButton(pxr::UsdAttribute attr, + pxr::UsdTimeCode editTime, + UsdLayerManager::CommandHistory* cmdHistory) +{ + using namespace pxr; + double t = editTime.GetValue(); + bool exactSample = HasExactTimeSample(attr, t); + + ImVec4 col = exactSample ? kColorKeyOn : kColorKeyOff; + ImGui::PushStyleColor(ImGuiCol_Text, col); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.f, 0.f, 0.f, 0.f)); + bool clicked = ImGui::SmallButton("(k)"); + ImGui::PopStyleColor(2); + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Set key at frame %.3g", t); + + if (!clicked || !cmdHistory) return; + + VtValue val; + if (!attr.Get(&val, editTime)) return; + + std::function executeFn = [attr, val, editTime]() mutable { + attr.Set(val, editTime); + }; + std::function undoFn; + if (exactSample) { + // overwrite: undo restores the same value that was there before + VtValue prev; + attr.Get(&prev, editTime); + undoFn = [attr, prev, editTime]() mutable { attr.Set(prev, editTime); }; + } else { + undoFn = [attr, editTime]() mutable { attr.ClearAtTime(editTime.GetValue()); }; + } + + cmdHistory->Push(std::make_unique( + "Set Key", std::move(executeFn), std::move(undoFn))); +} + // --------------------------------------------------------------------------- // Helper: mini-button (SmallButton coloured by authored state, like usdtweak) // Opens a popup context with "Copy path" on left-click. @@ -1109,35 +1173,112 @@ void PropertyPanel::RenderVariantSetsSection(const pxr::UsdPrim& prim) { } // --------------------------------------------------------------------------- -// Unified properties table (attributes + relationships) -// -// Mirrors ##DrawPropertyEditorTable in usdtweak DrawUsdPrimProperties: -// • Flags: SizingFixedFit | RowBg — NO borders, NO header row -// • Col 0 (fixed ~20px) : SmallButton mini-button coloured by authored state -// • Col 1 (fixed 140px) : property display name (namespace:basename) -// • Col 2 (stretch) : type-aware value widget, PushItemWidth(-FLT_MIN) +// Material binding section // --------------------------------------------------------------------------- -void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) { - // Exact flags used by usdtweak – no borders, alternating row background - constexpr ImGuiTableFlags kFlags = - ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg; +void PropertyPanel::RenderMaterialBindSection(const pxr::UsdPrim& prim) { + using namespace pxr; - if (!ImGui::BeginTable("##properties", 3, kFlags)) return; + if (!prim.HasAPI()) return; + UsdShadeMaterialBindingAPI bindAPI(prim); - ImGui::TableSetupColumn("##dot", ImGuiTableColumnFlags_WidthFixed, 20.f); - ImGui::TableSetupColumn("##name", ImGuiTableColumnFlags_WidthFixed, 140.f); + if (!ImGui::CollapsingHeader("Material Binding", ImGuiTreeNodeFlags_DefaultOpen)) + return; + + constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg; + if (!ImGui::BeginTable("##matbind", 2, kFlags)) return; + ImGui::TableSetupColumn("##mf", ImGuiTableColumnFlags_WidthFixed, 80.f); + ImGui::TableSetupColumn("##mv", ImGuiTableColumnFlags_WidthStretch); + + // Direct binding (allPurpose) + { + auto direct = bindAPI.GetDirectBinding(); + SdfPath matPath = direct.GetMaterialPath(); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); ImGui::TextDisabled("Direct"); + ImGui::TableSetColumnIndex(1); + if (!matPath.IsEmpty()) { + bool authored = direct.GetBindingRel() && direct.GetBindingRel().IsAuthored(); + ImVec4 col = authored ? ImVec4(0.6f, 0.9f, 1.f, 1.f) + : ImVec4(0.5f, 0.5f, 0.5f, 1.f); + ImGui::TextColored(col, "%s", matPath.GetText()); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", matPath.GetText()); + } else { + ImGui::TextDisabled("(none)"); + } + } + + // Preview purpose direct binding (only show if different from allPurpose) + { + auto preview = bindAPI.GetDirectBinding(UsdShadeTokens->preview); + SdfPath previewPath = preview.GetMaterialPath(); + if (!previewPath.IsEmpty()) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); ImGui::TextDisabled("Preview"); + ImGui::TableSetColumnIndex(1); + ImGui::TextColored(ImVec4(0.6f, 0.9f, 1.f, 1.f), "%s", previewPath.GetText()); + } + } + + // Full purpose direct binding + { + auto full = bindAPI.GetDirectBinding(UsdShadeTokens->full); + SdfPath fullPath = full.GetMaterialPath(); + if (!fullPath.IsEmpty()) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); ImGui::TextDisabled("Full"); + ImGui::TableSetColumnIndex(1); + ImGui::TextColored(ImVec4(0.6f, 0.9f, 1.f, 1.f), "%s", fullPath.GetText()); + } + } + + // Resolved (computed) bound material + { + UsdShadeMaterial resolved = bindAPI.ComputeBoundMaterial(); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); ImGui::TextDisabled("Resolved"); + ImGui::TableSetColumnIndex(1); + if (resolved) { + ImGui::TextColored(ImVec4(0.7f, 1.f, 0.7f, 1.f), + "%s", resolved.GetPrim().GetPath().GetText()); + } else { + ImGui::TextDisabled("(none)"); + } + } + + ImGui::EndTable(); +} + +// --------------------------------------------------------------------------- +// Inner attribute + relationship table — one category group +// --------------------------------------------------------------------------- +void PropertyPanel::RenderAttrRelSubTable( + const std::vector& attrs, + const std::vector& rels, + const pxr::UsdEditTarget& editTarget) +{ + using namespace pxr; + + if (attrs.empty() && rels.empty()) return; + + constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg; + if (!ImGui::BeginTable("##propSubTable", 3, kFlags)) return; + + // Col 0 wider when the key button column is active + float col0W = m_editTime.IsDefault() ? 20.f : 44.f; + ImGui::TableSetupColumn("##dot", ImGuiTableColumnFlags_WidthFixed, col0W); + ImGui::TableSetupColumn("##name", ImGuiTableColumnFlags_WidthFixed, 140.f); ImGui::TableSetupColumn("##val", ImGuiTableColumnFlags_WidthStretch); - // No TableHeadersRow – matches usdtweak "no header" intent - const pxr::UsdEditTarget& editTarget = prim.GetStage()->GetEditTarget(); const float rowH = ImGui::GetFrameHeight(); int uid = 0; - // ---- Attributes (mirrors usdtweak attribute loop) ---- - for (auto& attr : prim.GetAttributes()) { + // ── Attributes ────────────────────────────────────────────────────────── + for (auto attr : attrs) { ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH); - // Col 0 – mini button "(a)" + // Col 0 – (a) mini-button + optional (k) key button ImGui::TableSetColumnIndex(0); ImGui::PushID(uid++); DrawPropertyMiniButton("(a)", @@ -1146,29 +1287,75 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) { attr.GetTypeName().GetAsToken().GetText()); ImGui::PopID(); - // Col 1 – display name (namespace:basename) + bool isVarying = (attr.GetVariability() == SdfVariabilityVarying); + if (!m_editTime.IsDefault() && isVarying) { + ImGui::SameLine(0, 2); + ImGui::PushID(uid++); + DrawKeyButton(attr, m_editTime, m_commandHistory); + ImGui::PopID(); + } + + // Col 1 – display name + right-click context menu ImGui::TableSetColumnIndex(1); + ImGui::PushID(static_cast(attr.GetPath().GetHash()) ^ 0x100); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(GetPropDisplayName(attr).c_str()); + if (ImGui::BeginPopupContextItem("##attrCtx")) { + if (ImGui::MenuItem("Copy attribute path")) + ImGui::SetClipboardText(attr.GetPath().GetString().c_str()); + ImGui::Separator(); + bool authored = attr.IsAuthoredAt(editTarget); + ImGui::BeginDisabled(!authored); + if (ImGui::MenuItem("Clear Override")) { + // Capture current authored opinions so undo can restore them + std::vector times; + attr.GetTimeSamples(×); + std::vector> samples; + for (double t : times) { + VtValue v; + if (attr.Get(&v, UsdTimeCode(t))) + samples.emplace_back(t, v); + } + VtValue defVal; + bool hasDefault = attr.Get(&defVal, UsdTimeCode::Default()); - // Col 2 – value widget with PushItemWidth(-FLT_MIN) (label hidden) + if (m_commandHistory) { + m_commandHistory->Push(std::make_unique( + "Clear Override", + [attr]() mutable { attr.Clear(); }, + [attr, samps = samples, hasDefault, defVal]() mutable { + if (hasDefault) attr.Set(defVal, UsdTimeCode::Default()); + for (auto& [t, v] : samps) + attr.Set(v, UsdTimeCode(t)); + } + )); + } else { + attr.Clear(); + } + ImGui::CloseCurrentPopup(); + } + ImGui::EndDisabled(); + ImGui::EndPopup(); + } + ImGui::PopID(); + + // Col 2 – value widget ImGui::TableSetColumnIndex(2); ImGui::PushID(static_cast(attr.GetPath().GetHash())); ImGui::PushItemWidth(-FLT_MIN); - // Check for allowedTokens first → Combo (mirrors DrawTfToken in usdtweak) - pxr::VtValue allowedTokens; - attr.GetMetadata(pxr::TfToken("allowedTokens"), &allowedTokens); + VtValue allowedTokens; + attr.GetMetadata(TfToken("allowedTokens"), &allowedTokens); - pxr::VtValue val; + VtValue val; bool hasVal = attr.Get(&val, m_displayTime); if (!allowedTokens.IsEmpty() && - allowedTokens.IsHolding>()) + allowedTokens.IsHolding>()) { - const auto& tokens = allowedTokens.UncheckedGet>(); - std::string cur = (hasVal && val.IsHolding()) - ? val.UncheckedGet().GetString() : ""; + const auto& tokens = allowedTokens.UncheckedGet>(); + std::string cur = (hasVal && val.IsHolding()) + ? val.UncheckedGet().GetString() : ""; if (ImGui::BeginCombo("##tok", cur.c_str())) { for (const auto& tok : tokens) { bool sel = (tok.GetString() == cur); @@ -1178,8 +1365,7 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) { ImGui::EndCombo(); } } else if (hasVal) { - // Type-aware widget dispatch (mirrors DrawVtValue in usdtweak) - pxr::VtValue modified = DrawVtValueWidget(attr, val); + VtValue modified = DrawVtValueWidget(attr, val); if (!modified.IsEmpty()) attr.Set(modified, m_editTime); } else if (attr.HasAuthoredConnections()) { @@ -1198,11 +1384,11 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) { ImGui::PopID(); } - // ---- Relationships (mirrors usdtweak relationship loop) ---- - for (const auto& rel : prim.GetRelationships()) { + // ── Relationships ──────────────────────────────────────────────────────── + for (const auto& rel : rels) { ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH); - // Col 0 – mini button "(r)" + // Col 0 – (r) mini-button (no key button for relationships) ImGui::TableSetColumnIndex(0); ImGui::PushID(uid++); DrawPropertyMiniButton("(r)", @@ -1210,15 +1396,41 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) { rel.GetPath().GetString().c_str()); ImGui::PopID(); - // Col 1 – display name, coloured by authored state (mirrors DrawUsdRelationshipDisplayName) + // Col 1 – display name + right-click context menu ImGui::TableSetColumnIndex(1); + ImGui::PushID(static_cast(rel.GetPath().GetHash()) ^ 0x100); ImVec4 relColor = rel.IsAuthored() ? ImVec4(0.6f, 0.9f, 1.f, 1.f) : ImVec4(0.5f, 0.5f, 0.5f, 1.f); ImGui::AlignTextToFramePadding(); ImGui::TextColored(relColor, "%s", GetPropDisplayName(rel).c_str()); + if (ImGui::BeginPopupContextItem("##relCtx")) { + if (ImGui::MenuItem("Copy path")) + ImGui::SetClipboardText(rel.GetPath().GetString().c_str()); + ImGui::Separator(); + ImGui::BeginDisabled(!rel.IsAuthored()); + if (ImGui::MenuItem("Clear Override")) { + SdfPathVector oldTargets; + rel.GetTargets(&oldTargets); + if (m_commandHistory) { + m_commandHistory->Push(std::make_unique( + "Clear Override", + [rel]() mutable { rel.ClearTargets(false); }, + [rel, tgts = oldTargets]() mutable { + for (const auto& p : tgts) rel.AddTarget(p); + } + )); + } else { + rel.ClearTargets(false); + } + ImGui::CloseCurrentPopup(); + } + ImGui::EndDisabled(); + ImGui::EndPopup(); + } + ImGui::PopID(); - // Col 2 – target list: single row when collapsed, expand for multiple + // Col 2 – target list ImGui::TableSetColumnIndex(2); SdfPathVector targets; rel.GetTargets(&targets); @@ -1253,6 +1465,113 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) { ImGui::EndTable(); } +// --------------------------------------------------------------------------- +// Unified properties table (attributes + relationships) +// +// Mirrors ##DrawPropertyEditorTable in usdtweak DrawUsdPrimProperties: +// • Flags: SizingFixedFit | RowBg — NO borders, NO header row +// • Col 0 (fixed ~20px) : SmallButton mini-button coloured by authored state +// • Col 1 (fixed 140px) : property display name (namespace:basename) +// • Col 2 (stretch) : type-aware value widget, PushItemWidth(-FLT_MIN) +// --------------------------------------------------------------------------- +void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) { + using namespace pxr; + + // Build attr-name → category label map from schema registry + std::unordered_map attrToCategory; + const UsdSchemaRegistry& reg = UsdSchemaRegistry::GetInstance(); + + if (!m_primType.empty()) { + if (const UsdPrimDefinition* typeDef = + reg.FindConcretePrimDefinition(TfToken(m_primType))) { + for (const TfToken& prop : typeDef->GetPropertyNames()) + attrToCategory.emplace(prop.GetString(), m_primType); + } + } + + for (const auto& schemaToken : prim.GetAppliedSchemas()) { + std::string s = schemaToken.GetString(); + // Multi-apply tokens look like "CollectionAPI:name"; strip instance suffix + auto pos = s.find(':'); + std::string baseName = (pos != std::string::npos) ? s.substr(0, pos) : s; + + if (const UsdPrimDefinition* apiDef = + reg.FindAppliedAPIPrimDefinition(TfToken(baseName))) { + for (const TfToken& prop : apiDef->GetPropertyNames()) + attrToCategory.emplace(prop.GetString(), baseName); // first writer wins + } + } + + // Bucket attrs and rels into ordered category groups + struct PropGroup { + std::vector attrs; + std::vector rels; + }; + std::vector orderedCats; + std::set seenCats; + std::unordered_map groups; + + auto addCat = [&](const std::string& cat) { + if (seenCats.insert(cat).second) orderedCats.push_back(cat); + }; + if (!m_primType.empty()) addCat(m_primType); + + const std::string fallback = m_primType.empty() ? "Properties" : m_primType; + + for (auto attr : prim.GetAttributes()) { + const std::string name = attr.GetName().GetString(); + if (TfStringStartsWith(name, "xformOp:")) continue; // shown in TRS section + + auto it = attrToCategory.find(name); + std::string cat; + if (it != attrToCategory.end()) { + cat = it->second; + } else { + std::string ns = attr.GetNamespace().GetString(); + if (ns.empty()) { + cat = fallback; + } else { + auto c = ns.find(':'); + cat = (c != std::string::npos) ? ns.substr(0, c) : ns; + } + } + addCat(cat); + groups[cat].attrs.push_back(attr); + } + + for (const auto& rel : prim.GetRelationships()) { + std::string ns = rel.GetNamespace().GetString(); + std::string cat; + if (ns.empty()) { + cat = fallback; + } else { + auto c = ns.find(':'); + cat = (c != std::string::npos) ? ns.substr(0, c) : ns; + } + addCat(cat); + groups[cat].rels.push_back(rel); + } + + // Sort: primType first (already inserted), remaining alphabetically + if (orderedCats.size() > 1) { + size_t firstSort = m_primType.empty() ? 0 : 1; + std::sort(orderedCats.begin() + firstSort, orderedCats.end()); + } + + // Render each category under a collapsing header + const UsdEditTarget& editTarget = prim.GetStage()->GetEditTarget(); + + for (const auto& cat : orderedCats) { + auto& group = groups[cat]; + if (group.attrs.empty() && group.rels.empty()) continue; + + ImGui::PushID(cat.c_str()); + if (ImGui::CollapsingHeader(cat.c_str(), ImGuiTreeNodeFlags_DefaultOpen)) + RenderAttrRelSubTable(group.attrs, group.rels, editTarget); + ImGui::PopID(); + } +} + // --------------------------------------------------------------------------- // Main render entry point // --------------------------------------------------------------------------- @@ -1289,6 +1608,11 @@ void PropertyPanel::Render() { ImGui::Separator(); } + if (prim.HasAPI()) { + RenderMaterialBindSection(prim); + ImGui::Separator(); + } + if (m_hasXform) { RenderTransformSection(); ImGui::Separator(); diff --git a/src/ui/PropertyPanel.h b/src/ui/PropertyPanel.h index 003d788..3e16eb3 100644 --- a/src/ui/PropertyPanel.h +++ b/src/ui/PropertyPanel.h @@ -52,7 +52,11 @@ private: void RenderPrimHeader(const pxr::UsdPrim& prim); ///< fixed-height header child void RenderVariantSetsSection(const pxr::UsdPrim& prim); void RenderTransformSection(); - void RenderPropertiesTable(const pxr::UsdPrim& prim); ///< unified attr+rel table + void RenderPropertiesTable(const pxr::UsdPrim& prim); ///< groups attrs by API schema + void RenderMaterialBindSection(const pxr::UsdPrim& prim); ///< material:binding info + 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;