diff --git a/src/core/commands/RenamePrimCommand.cpp b/src/core/commands/RenamePrimCommand.cpp index 14fbb1f..6694171 100644 --- a/src/core/commands/RenamePrimCommand.cpp +++ b/src/core/commands/RenamePrimCommand.cpp @@ -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); diff --git a/src/ui/SceneHierarchyPanel.cpp b/src/ui/SceneHierarchyPanel.cpp index 60ce015..10f9ff5 100644 --- a/src/ui/SceneHierarchyPanel.cpp +++ b/src/ui/SceneHierarchyPanel.cpp @@ -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 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( + 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( + m_stage, prim.GetPath(), SdfPath("/"), name)); + SetSelectedPathFromClick(SdfPath("/").AppendChild(TfToken(name)).GetString()); + } + } if (ImGui::MenuItem("Group", "Ctrl+G")) { std::vector 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();