Fix prim expiry crashes and add context menu hierarchy ops

Crash fixes (expired UsdPrim after namespace edits):
- RenderPrimNode: early-out after rename commit (renamed flag + column cleanup)
- RenderPrimNode: early-out after context menu op expires prim (prim.IsValid() guard)
- RenderContextMenu: EndPopup+return when Unparent/Group expires prim mid-popup
- Child iteration: snapshot GetChildren() into vector before recursing to guard
  against live iterator invalidation from stage mutations deeper in the tree
- Rename: firstFrame guard prevents spurious IsItemDeactivated cancel on
  SetKeyboardFocusHere activation frame (fixes second-rename not working)
- RenamePrimCommand: check RenamePrim() return value, log if rejected

New features:
- Context menu Add Child submenu: create typed child prim under selection
- Context menu Unparent to Root: move prim to stage root (undo-able)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 03:43:01 +08:00
parent b31ca69d4f
commit b02a736da0
2 changed files with 97 additions and 5 deletions
+8 -2
View File
@@ -27,7 +27,10 @@ void RenamePrimCommand::Execute() {
return; return;
} }
UsdNamespaceEditor editor(m_stage); UsdNamespaceEditor editor(m_stage);
editor.RenamePrim(prim, TfToken(m_newName)); if (!editor.RenamePrim(prim, TfToken(m_newName))) {
LOG_ERROR("RenamePrimCommand::Execute: RenamePrim() rejected for " + m_oldPath.GetString());
return;
}
std::string whyNot; std::string whyNot;
if (!editor.CanApplyEdits(&whyNot)) { if (!editor.CanApplyEdits(&whyNot)) {
LOG_ERROR("RenamePrimCommand::Execute: cannot rename " + m_oldPath.GetString() + ": " + whyNot); LOG_ERROR("RenamePrimCommand::Execute: cannot rename " + m_oldPath.GetString() + ": " + whyNot);
@@ -45,7 +48,10 @@ void RenamePrimCommand::Undo() {
return; return;
} }
UsdNamespaceEditor editor(m_stage); UsdNamespaceEditor editor(m_stage);
editor.RenamePrim(prim, TfToken(m_oldName)); if (!editor.RenamePrim(prim, TfToken(m_oldName))) {
LOG_ERROR("RenamePrimCommand::Undo: RenamePrim() rejected for " + m_newPath.GetString());
return;
}
std::string whyNot; std::string whyNot;
if (!editor.CanApplyEdits(&whyNot)) { if (!editor.CanApplyEdits(&whyNot)) {
LOG_ERROR("RenamePrimCommand::Undo: cannot rename back " + m_newPath.GetString() + ": " + whyNot); LOG_ERROR("RenamePrimCommand::Undo: cannot rename back " + m_newPath.GetString() + ": " + whyNot);
+89 -3
View File
@@ -597,7 +597,8 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
if (!hasChildren) flags |= ImGuiTreeNodeFlags_Leaf | if (!hasChildren) flags |= ImGuiTreeNodeFlags_Leaf |
ImGuiTreeNodeFlags_NoTreePushOnOpen; ImGuiTreeNodeFlags_NoTreePushOnOpen;
bool open = false; bool open = false;
bool renamed = false; // true if rename committed this frame (prim now expired)
if (isRenaming) { if (isRenaming) {
// Render just the arrow (hidden label) so we keep indentation and open/close, // Render just the arrow (hidden label) so we keep indentation and open/close,
// then overlay an InputText inline for the prim name. // then overlay an InputText inline for the prim name.
@@ -606,6 +607,9 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
ImGui::PopStyleColor(); ImGui::PopStyleColor();
ImGui::SameLine(); ImGui::SameLine();
ImGui::SetNextItemWidth(-1.0f); ImGui::SetNextItemWidth(-1.0f);
// firstFrame: skip deactivation-cancel on the exact frame SetKeyboardFocusHere
// activates the widget — IsItemDeactivated() can fire spuriously on that frame.
bool firstFrame = m_renameJustStarted;
if (m_renameJustStarted) { if (m_renameJustStarted) {
ImGui::SetKeyboardFocusHere(); ImGui::SetKeyboardFocusHere();
m_renameJustStarted = false; m_renameJustStarted = false;
@@ -613,7 +617,7 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
bool commit = ImGui::InputText("##rename_input", m_renameBuf, sizeof(m_renameBuf), bool commit = ImGui::InputText("##rename_input", m_renameBuf, sizeof(m_renameBuf),
ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_EnterReturnsTrue |
ImGuiInputTextFlags_AutoSelectAll); ImGuiInputTextFlags_AutoSelectAll);
bool canceled = ImGui::IsItemDeactivated() && !commit; bool canceled = !firstFrame && ImGui::IsItemDeactivated() && !commit;
if (commit && m_renameBuf[0] != '\0') { if (commit && m_renameBuf[0] != '\0') {
std::string newName = SanitizeUsdName(m_renameBuf); std::string newName = SanitizeUsdName(m_renameBuf);
if (!newName.empty() && newName != displayName && m_commandHistory) { if (!newName.empty() && newName != displayName && m_commandHistory) {
@@ -621,6 +625,7 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
m_stage, primPath, newName)); m_stage, primPath, newName));
SdfPath newPath = primPath.GetParentPath().AppendChild(TfToken(newName)); SdfPath newPath = primPath.GetParentPath().AppendChild(TfToken(newName));
SetSelectedPathFromClick(newPath.GetString()); SetSelectedPathFromClick(newPath.GetString());
renamed = true; // prim at primPath is now expired — stop here
} }
m_renamingPath = SdfPath(); m_renamingPath = SdfPath();
} }
@@ -632,6 +637,17 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} }
// ── Early-out after rename: prim is expired, close table row and return ──
if (renamed) {
ImGui::TableNextColumn(); // Col 1
ImGui::TableNextColumn(); // Col 2
ImGui::TableNextColumn(); // Col 3
if (open && hasChildren)
ImGui::TreePop();
ImGui::PopID();
return;
}
// ── Scroll-to-selection (SpanAllColumns gives correct full-row rect) ──── // ── Scroll-to-selection (SpanAllColumns gives correct full-row rect) ────
if (m_scrollToSelected && primStr == m_primarySelectedPath) { if (m_scrollToSelected && primStr == m_primarySelectedPath) {
ImGui::SetScrollHereY(0.5f); ImGui::SetScrollHereY(0.5f);
@@ -691,6 +707,18 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
// Context menu (must follow the last widget = the tree node). // Context menu (must follow the last widget = the tree node).
RenderContextMenu(prim); RenderContextMenu(prim);
// A context menu op (Unparent, Group, etc.) may have moved this prim,
// expiring it. Bail out before touching the dead prim object.
if (!prim.IsValid()) {
ImGui::TableNextColumn(); // Col 1
ImGui::TableNextColumn(); // Col 2
ImGui::TableNextColumn(); // Col 3
if (open && hasChildren)
ImGui::TreePop();
ImGui::PopID();
return;
}
} }
// ── Col 1: Prim-type icon ─────────────────────────────────────────────── // ── Col 1: Prim-type icon ───────────────────────────────────────────────
@@ -769,7 +797,11 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
// Solution: recurse here (after columns), but ImGui only needs TreePop to // Solution: recurse here (after columns), but ImGui only needs TreePop to
// be inside the same Begin/End pair — column doesn't matter for TreePop. // be inside the same Begin/End pair — column doesn't matter for TreePop.
if (open && hasChildren) { if (open && hasChildren) {
for (const auto& child : prim.GetChildren()) // Snapshot children: stage mutations inside RenderPrimNode (reparent, group)
// invalidate the live GetChildren() iterator.
std::vector<UsdPrim> children(prim.GetChildren().begin(),
prim.GetChildren().end());
for (const auto& child : children)
RenderPrimNode(child); RenderPrimNode(child);
ImGui::TreePop(); ImGui::TreePop();
} }
@@ -817,6 +849,44 @@ void SceneHierarchyPanel::RenderContextMenu(const UsdPrim& prim) {
ImGui::Separator(); ImGui::Separator();
// ── Add Child Prim ────────────────────────────────────────────────────
static const char* kChildPrimTypes[] = {
"Xform", "Scope",
"Mesh", "Sphere", "Cube", "Cylinder", "Cone", "Capsule",
"Camera",
"SphereLight", "DomeLight", "RectLight", "DiskLight",
"CylinderLight", "DistantLight"
};
if (ImGui::BeginMenu("Add Child")) {
SdfPath parentPath = prim.GetPath();
for (const char* typeName : kChildPrimTypes) {
if (ImGui::MenuItem(typeName)) {
std::string baseName = typeName;
SdfPath childPath = FindUniqueChildPath(parentPath, baseName);
if (m_commandHistory) {
m_commandHistory->Push(std::make_unique<CreatePrimCommand>(
m_stage, childPath, TfToken(typeName)));
UsdPrim child = m_stage->GetPrimAtPath(childPath);
if (child.IsValid()) {
SetSelectedPathFromClick(childPath.GetString());
m_scrollToSelected = true;
}
} else {
UsdPrim child = m_stage->DefinePrim(childPath, TfToken(typeName));
if (child.IsValid()) {
SetSelectedPathFromClick(childPath.GetString());
m_scrollToSelected = true;
} else {
LOG_ERROR("Failed to create child prim: " + childPath.GetString());
}
}
}
}
ImGui::EndMenu();
}
ImGui::Separator();
// ── Rename / Group ──────────────────────────────────────────────────── // ── Rename / Group ────────────────────────────────────────────────────
if (ImGui::MenuItem("Rename", "F2")) { if (ImGui::MenuItem("Rename", "F2")) {
m_renamingPath = prim.GetPath(); m_renamingPath = prim.GetPath();
@@ -826,6 +896,15 @@ void SceneHierarchyPanel::RenderContextMenu(const UsdPrim& prim) {
m_renameJustStarted = true; m_renameJustStarted = true;
m_scrollToSelected = true; m_scrollToSelected = true;
} }
bool canUnparent = (prim.GetPath().GetPathElementCount() > 1);
if (ImGui::MenuItem("Unparent to Root", nullptr, false, canUnparent)) {
if (m_commandHistory && m_stage) {
std::string name = FindUniqueChildPath(SdfPath("/"), prim.GetName().GetString()).GetName();
m_commandHistory->Push(std::make_unique<ReparentPrimCommand>(
m_stage, prim.GetPath(), SdfPath("/"), name));
SetSelectedPathFromClick(SdfPath("/").AppendChild(TfToken(name)).GetString());
}
}
if (ImGui::MenuItem("Group", "Ctrl+G")) { if (ImGui::MenuItem("Group", "Ctrl+G")) {
std::vector<SdfPath> srcs; std::vector<SdfPath> srcs;
if (m_selectedPaths.count(prim.GetPath().GetString())) if (m_selectedPaths.count(prim.GetPath().GetString()))
@@ -852,6 +931,13 @@ void SceneHierarchyPanel::RenderContextMenu(const UsdPrim& prim) {
} }
} }
// Unparent or Group may have moved this prim, expiring it.
// Close the popup now before touching any more prim state.
if (!prim.IsValid()) {
ImGui::EndPopup();
return;
}
ImGui::Separator(); ImGui::Separator();
bool hasChildren = !prim.GetChildren().empty(); bool hasChildren = !prim.GetChildren().empty();