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;
|
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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user