diff --git a/src/ui/SceneHierarchyPanel.cpp b/src/ui/SceneHierarchyPanel.cpp index 22ce16d..e90d4ca 100644 --- a/src/ui/SceneHierarchyPanel.cpp +++ b/src/ui/SceneHierarchyPanel.cpp @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -400,6 +403,9 @@ void SceneHierarchyPanel::Render() { // Deferred confirm modal for prim removal (must be opened outside any popup stack). RenderRemovePrimModal(); + // Deferred modal for arc operations (Inherit / Specialize / VariantSet / Variant). + RenderArcModal(); + // Deferred file-dialog for reference replacement (must run outside any popup stack). ProcessPendingReplaceRef(); } @@ -807,6 +813,136 @@ void SceneHierarchyPanel::RenderContextMenu(const UsdPrim& prim) { ImGui::EndMenu(); } + // ---- Inherit submenu ---- + bool hasInherits = prim.HasAuthoredInherits(); + if (ImGui::BeginMenu("Inherit")) { + if (ImGui::MenuItem("Add...")) { + m_arcModalMode = ArcModalMode::Inherit; + m_arcModalTargetPrim = prim.GetPath(); + m_arcModalBuf[0] = '\0'; + m_openArcModal = true; + } + ImGui::Separator(); + if (ImGui::BeginMenu("Remove", hasInherits)) { + SdfPathVector paths = prim.GetInherits().GetAllDirectInherits(); + bool any = false; + for (const SdfPath& ipath : paths) { + if (ImGui::MenuItem(ipath.GetText())) { + try { + prim.GetInherits().RemoveInherit(ipath); + LOG_INFO("Removed inherit '" + ipath.GetString() + "' from: " + prim.GetPath().GetString()); + } catch (const std::exception& e) { + LOG_ERROR(std::string("Remove inherit error: ") + e.what()); + } + } + any = true; + } + if (any) ImGui::Separator(); + if (ImGui::MenuItem("Clear All")) { + try { + prim.GetInherits().ClearInherits(); + LOG_INFO("Cleared all inherits on: " + prim.GetPath().GetString()); + } catch (const std::exception& e) { + LOG_ERROR(std::string("Clear inherits error: ") + e.what()); + } + } + ImGui::EndMenu(); + } + ImGui::EndMenu(); + } + + // ---- Specialize submenu ---- + bool hasSpecializes = prim.HasAuthoredSpecializes(); + if (ImGui::BeginMenu("Specialize")) { + if (ImGui::MenuItem("Add...")) { + m_arcModalMode = ArcModalMode::Specialize; + m_arcModalTargetPrim = prim.GetPath(); + m_arcModalBuf[0] = '\0'; + m_openArcModal = true; + } + ImGui::Separator(); + if (ImGui::BeginMenu("Remove", hasSpecializes)) { + UsdPrimCompositionQuery::Filter filter; + filter.arcTypeFilter = UsdPrimCompositionQuery::ArcTypeFilter::Specialize; + filter.dependencyTypeFilter = UsdPrimCompositionQuery::DependencyTypeFilter::Direct; + UsdPrimCompositionQuery query(prim, filter); + + bool any = false; + for (auto& arc : query.GetCompositionArcs()) { + SdfPathEditorProxy editor; + SdfPath specPath; + if (arc.GetIntroducingListEditor(&editor, &specPath)) { + if (ImGui::MenuItem(specPath.GetText())) { + try { + prim.GetSpecializes().RemoveSpecialize(specPath); + LOG_INFO("Removed specialize '" + specPath.GetString() + "' from: " + prim.GetPath().GetString()); + } catch (const std::exception& e) { + LOG_ERROR(std::string("Remove specialize error: ") + e.what()); + } + } + any = true; + } + } + if (any) ImGui::Separator(); + if (ImGui::MenuItem("Clear All")) { + try { + prim.GetSpecializes().ClearSpecializes(); + LOG_INFO("Cleared all specializes on: " + prim.GetPath().GetString()); + } catch (const std::exception& e) { + LOG_ERROR(std::string("Clear specializes error: ") + e.what()); + } + } + ImGui::EndMenu(); + } + ImGui::EndMenu(); + } + + // ---- VariantSet submenu ---- + if (ImGui::BeginMenu("VariantSet")) { + if (ImGui::MenuItem("Add...")) { + m_arcModalMode = ArcModalMode::VariantSet; + m_arcModalTargetPrim = prim.GetPath(); + m_arcModalBuf[0] = '\0'; + m_openArcModal = true; + } + bool hasVarSets = prim.HasVariantSets(); + if (hasVarSets) { + ImGui::Separator(); + std::vector vsNames; + prim.GetVariantSets().GetNames(&vsNames); + for (const std::string& vsName : vsNames) { + if (ImGui::BeginMenu(vsName.c_str())) { + if (ImGui::MenuItem("Add Variant...")) { + m_arcModalMode = ArcModalMode::Variant; + m_arcModalTargetPrim = prim.GetPath(); + m_arcModalVarSetName = vsName; + m_arcModalBuf[0] = '\0'; + m_openArcModal = true; + } + UsdVariantSet vs = prim.GetVariantSet(vsName); + std::vector varNames = vs.GetVariantNames(); + if (!varNames.empty()) { + ImGui::Separator(); + std::string currentSel = vs.GetVariantSelection(); + for (const std::string& varName : varNames) { + bool isCurrent = (varName == currentSel); + if (ImGui::MenuItem(varName.c_str(), nullptr, isCurrent)) { + try { + vs.SetVariantSelection(varName); + LOG_INFO("Set variant '" + varName + "' on '" + vsName + "' for: " + prim.GetPath().GetString()); + } catch (const std::exception& e) { + LOG_ERROR(std::string("Set variant selection error: ") + e.what()); + } + } + } + } + ImGui::EndMenu(); + } + } + } + ImGui::EndMenu(); + } + // ---- Prim removal ---- // Only show "Remove Prim" for prims that have a local spec authored in the root // layer. Prims brought in purely via composition from an external referenced @@ -909,6 +1045,149 @@ void SceneHierarchyPanel::RenderRemovePrimModal() { } } +void SceneHierarchyPanel::RenderArcModal() { + if (m_openArcModal) { + ImGui::OpenPopup("Arc Operation##arc"); + m_openArcModal = false; + } + + ImVec2 center = ImGui::GetMainViewport()->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(420, 0), ImGuiCond_Always); + + if (ImGui::BeginPopupModal("Arc Operation##arc", nullptr, + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) { + + const char* title = "Arc Operation"; + const char* prompt = "Path:"; + bool isNameInput = false; + + switch (m_arcModalMode) { + case ArcModalMode::Inherit: + title = "Add Inherit"; + prompt = "Inherit path (e.g. /ClassName):"; + break; + case ArcModalMode::Specialize: + title = "Add Specialize"; + prompt = "Specialize path (e.g. /BasePrim):"; + break; + case ArcModalMode::VariantSet: + title = "Add VariantSet"; + prompt = "VariantSet name:"; + isNameInput = true; + break; + case ArcModalMode::Variant: + title = "Add Variant"; + prompt = "Variant name:"; + isNameInput = true; + break; + default: + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + return; + } + + ImGui::TextUnformatted(title); + ImGui::Separator(); + ImGui::Spacing(); + ImGui::TextDisabled("Prim: %s", m_arcModalTargetPrim.GetText()); + if (m_arcModalMode == ArcModalMode::Variant) + ImGui::TextDisabled("VariantSet: %s", m_arcModalVarSetName.c_str()); + ImGui::Spacing(); + ImGui::TextUnformatted(prompt); + ImGui::SetNextItemWidth(-1.0f); + if (ImGui::IsWindowAppearing()) + ImGui::SetKeyboardFocusHere(); + bool confirm = ImGui::InputText("##arcbuf", m_arcModalBuf, + sizeof(m_arcModalBuf), + ImGuiInputTextFlags_EnterReturnsTrue); + ImGui::Spacing(); + ImGui::Separator(); + + bool inputNonEmpty = (m_arcModalBuf[0] != '\0'); + float buttonWidth = 120.0f; + float spacing = ImGui::GetStyle().ItemSpacing.x; + float totalW = buttonWidth * 2.0f + spacing; + ImGui::SetCursorPosX( + (ImGui::GetContentRegionAvail().x - totalW) * 0.5f + ImGui::GetCursorPosX()); + + // Capture Enter-confirm before BeginDisabled so it works regardless of button state. + bool doConfirm = (confirm && inputNonEmpty); + + if (!inputNonEmpty) ImGui::BeginDisabled(); + if (ImGui::Button("OK", ImVec2(buttonWidth, 0))) + doConfirm = inputNonEmpty; + if (!inputNonEmpty) ImGui::EndDisabled(); + + ImGui::SameLine(); + + if (ImGui::Button("Cancel", ImVec2(buttonWidth, 0))) { + m_arcModalMode = ArcModalMode::None; + m_arcModalTargetPrim = SdfPath(); + m_arcModalVarSetName.clear(); + ImGui::CloseCurrentPopup(); + } + + if (doConfirm) { + std::string raw(m_arcModalBuf); + std::string val = isNameInput ? SanitizeUsdName(raw) : raw; + + if (m_stage && !m_arcModalTargetPrim.IsEmpty()) { + UsdPrim prim = m_stage->GetPrimAtPath(m_arcModalTargetPrim); + if (prim.IsValid()) { + try { + switch (m_arcModalMode) { + case ArcModalMode::Inherit: { + SdfPath ipath(val); + bool ok = prim.GetInherits().AddInherit(ipath); + if (ok) + LOG_INFO("Added inherit '" + val + "' to: " + m_arcModalTargetPrim.GetString()); + else + LOG_ERROR("Failed to add inherit '" + val + "' to: " + m_arcModalTargetPrim.GetString()); + break; + } + case ArcModalMode::Specialize: { + SdfPath spath(val); + bool ok = prim.GetSpecializes().AddSpecialize(spath); + if (ok) + LOG_INFO("Added specialize '" + val + "' to: " + m_arcModalTargetPrim.GetString()); + else + LOG_ERROR("Failed to add specialize '" + val + "' to: " + m_arcModalTargetPrim.GetString()); + break; + } + case ArcModalMode::VariantSet: { + prim.GetVariantSets().AddVariantSet(val); + LOG_INFO("Added variantSet '" + val + "' to: " + m_arcModalTargetPrim.GetString()); + break; + } + case ArcModalMode::Variant: { + UsdVariantSet vs = prim.GetVariantSet(m_arcModalVarSetName); + bool ok = vs.AddVariant(val); + if (ok) + LOG_INFO("Added variant '" + val + "' to variantSet '" + m_arcModalVarSetName + "' on: " + m_arcModalTargetPrim.GetString()); + else + LOG_ERROR("Failed to add variant '" + val + "' to variantSet '" + m_arcModalVarSetName + "'"); + break; + } + default: break; + } + } catch (const std::exception& e) { + LOG_ERROR(std::string("Arc operation error: ") + e.what()); + } + } else { + LOG_ERROR("Arc modal: prim no longer valid: " + m_arcModalTargetPrim.GetString()); + } + } + m_arcModalMode = ArcModalMode::None; + m_arcModalTargetPrim = SdfPath(); + m_arcModalVarSetName.clear(); + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } +} + void SceneHierarchyPanel::ProcessPendingReplaceRef() { if (!m_doReplaceRefPick) return; m_doReplaceRefPick = false; diff --git a/src/ui/SceneHierarchyPanel.h b/src/ui/SceneHierarchyPanel.h index d8d3834..42785cc 100644 --- a/src/ui/SceneHierarchyPanel.h +++ b/src/ui/SceneHierarchyPanel.h @@ -66,12 +66,15 @@ public: using StageMetadataChangedCallback = std::function; void SetOnStageMetadataChanged(StageMetadataChangedCallback callback) { m_onStageMetadataChanged = callback; } + enum class ArcModalMode { None, Inherit, Specialize, VariantSet, Variant }; + private: void RenderPrimNode(const UsdPrim& prim); const char* GetPrimTypeIcon(const UsdPrim& prim) const; Icon GetPrimTypeIconEnum(const UsdPrim& prim) const; void RenderContextMenu(const UsdPrim& prim); void RenderRemovePrimModal(); + void RenderArcModal(); void ProcessPendingReplaceRef(); PropertyManager* m_propertyManager; @@ -104,6 +107,13 @@ private: bool m_doReplaceRefPick = false; SdfPath m_pendingReplaceRefPrim; pxr::SdfReference m_pendingReplaceRef; + + /// Arc modal state (Inherit / Specialize / VariantSet / Variant). + ArcModalMode m_arcModalMode = ArcModalMode::None; + SdfPath m_arcModalTargetPrim; + std::string m_arcModalVarSetName; + char m_arcModalBuf[512] = {}; + bool m_openArcModal = false; }; } // namespace UsdLayerManager