Scene hierarchy: add Inherit, Specialize, and VariantSet composition arc menus

Prim right-click context menu now exposes all five USD composition arc types:

- Inherit▶  Add... (modal for SdfPath input) | Remove▶ (lists via GetAllDirectInherits()) | Clear All
- Specialize▶  Add... | Remove▶ (lists via UsdPrimCompositionQuery + ArcTypeFilter::Specialize) | Clear All
- VariantSet▶  Add... | per-set sub-menu: Add Variant... + selectable variant list (SetVariantSelection)

All text-input operations share a single RenderArcModal() driven by ArcModalMode enum.
VariantSet/Variant names go through SanitizeUsdName(); Inherit/Specialize paths are passed
as-is to SdfPath. OK button is disabled until input is non-empty; Enter key also confirms.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 04:48:18 +08:00
parent 3ba00761c1
commit 81d4372bf6
2 changed files with 289 additions and 0 deletions
+279
View File
@@ -9,6 +9,9 @@
#include <pxr/usd/usd/primRange.h>
#include <pxr/usd/usd/references.h>
#include <pxr/usd/usd/payloads.h>
#include <pxr/usd/usd/inherits.h>
#include <pxr/usd/usd/specializes.h>
#include <pxr/usd/usd/variantSets.h>
#include <pxr/usd/usd/primCompositionQuery.h>
#include <pxr/usd/sdf/payload.h>
#include <pxr/usd/usdGeom/imageable.h>
@@ -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<std::string> 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<std::string> 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;