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:
@@ -27,7 +27,10 @@ void RenamePrimCommand::Execute() {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
if (!editor.CanApplyEdits(&whyNot)) {
|
||||
LOG_ERROR("RenamePrimCommand::Execute: cannot rename " + m_oldPath.GetString() + ": " + whyNot);
|
||||
@@ -45,7 +48,10 @@ void RenamePrimCommand::Undo() {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
if (!editor.CanApplyEdits(&whyNot)) {
|
||||
LOG_ERROR("RenamePrimCommand::Undo: cannot rename back " + m_newPath.GetString() + ": " + whyNot);
|
||||
|
||||
@@ -597,7 +597,8 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
|
||||
if (!hasChildren) flags |= ImGuiTreeNodeFlags_Leaf |
|
||||
ImGuiTreeNodeFlags_NoTreePushOnOpen;
|
||||
|
||||
bool open = false;
|
||||
bool open = false;
|
||||
bool renamed = false; // true if rename committed this frame (prim now expired)
|
||||
if (isRenaming) {
|
||||
// Render just the arrow (hidden label) so we keep indentation and open/close,
|
||||
// then overlay an InputText inline for the prim name.
|
||||
@@ -606,6 +607,9 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::SameLine();
|
||||
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) {
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
m_renameJustStarted = false;
|
||||
@@ -613,7 +617,7 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
|
||||
bool commit = ImGui::InputText("##rename_input", m_renameBuf, sizeof(m_renameBuf),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue |
|
||||
ImGuiInputTextFlags_AutoSelectAll);
|
||||
bool canceled = ImGui::IsItemDeactivated() && !commit;
|
||||
bool canceled = !firstFrame && ImGui::IsItemDeactivated() && !commit;
|
||||
if (commit && m_renameBuf[0] != '\0') {
|
||||
std::string newName = SanitizeUsdName(m_renameBuf);
|
||||
if (!newName.empty() && newName != displayName && m_commandHistory) {
|
||||
@@ -621,6 +625,7 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
|
||||
m_stage, primPath, newName));
|
||||
SdfPath newPath = primPath.GetParentPath().AppendChild(TfToken(newName));
|
||||
SetSelectedPathFromClick(newPath.GetString());
|
||||
renamed = true; // prim at primPath is now expired — stop here
|
||||
}
|
||||
m_renamingPath = SdfPath();
|
||||
}
|
||||
@@ -632,6 +637,17 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
|
||||
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) ────
|
||||
if (m_scrollToSelected && primStr == m_primarySelectedPath) {
|
||||
ImGui::SetScrollHereY(0.5f);
|
||||
@@ -691,6 +707,18 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
|
||||
|
||||
// Context menu (must follow the last widget = the tree node).
|
||||
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 ───────────────────────────────────────────────
|
||||
@@ -769,7 +797,11 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
|
||||
// Solution: recurse here (after columns), but ImGui only needs TreePop to
|
||||
// be inside the same Begin/End pair — column doesn't matter for TreePop.
|
||||
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);
|
||||
ImGui::TreePop();
|
||||
}
|
||||
@@ -817,6 +849,44 @@ void SceneHierarchyPanel::RenderContextMenu(const UsdPrim& prim) {
|
||||
|
||||
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 ────────────────────────────────────────────────────
|
||||
if (ImGui::MenuItem("Rename", "F2")) {
|
||||
m_renamingPath = prim.GetPath();
|
||||
@@ -826,6 +896,15 @@ void SceneHierarchyPanel::RenderContextMenu(const UsdPrim& prim) {
|
||||
m_renameJustStarted = 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")) {
|
||||
std::vector<SdfPath> srcs;
|
||||
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();
|
||||
|
||||
bool hasChildren = !prim.GetChildren().empty();
|
||||
|
||||
Reference in New Issue
Block a user