diff --git a/src/core/commands/GroupPrimsCommand.cpp b/src/core/commands/GroupPrimsCommand.cpp new file mode 100644 index 0000000..9b82f81 --- /dev/null +++ b/src/core/commands/GroupPrimsCommand.cpp @@ -0,0 +1,113 @@ +#include "GroupPrimsCommand.h" +#include "../../utils/Logger.h" +#include +#include +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace UsdLayerManager { + +GroupPrimsCommand::GroupPrimsCommand(UsdStageRefPtr stage, + const std::vector& srcPaths, + const SdfPath& groupPath) + : m_stage(stage) + , m_groupPath(groupPath) + , m_srcPaths(srcPaths) + , m_description("Group " + std::to_string(srcPaths.size()) + + " prim(s) under " + groupPath.GetString()) +{ +} + +/// Compute a unique child name under @p parent (collision check against live stage). +static std::string UniqueNameUnder(UsdStageRefPtr stage, + const SdfPath& parent, + const std::string& baseName) { + std::string name = baseName; + int n = 1; + while (stage->GetPrimAtPath(parent.AppendChild(TfToken(name))).IsValid()) + name = baseName + "_" + std::to_string(n++); + return name; +} + +static UsdPrim GetParentPrim(UsdStageRefPtr stage, const SdfPath& parentPath) { + if (parentPath == SdfPath("/") || parentPath == SdfPath::AbsoluteRootPath()) + return stage->GetPseudoRoot(); + return stage->GetPrimAtPath(parentPath); +} + +void GroupPrimsCommand::Execute() { + // Create the Xform group. + UsdPrim groupPrim = m_stage->DefinePrim(m_groupPath, TfToken("Xform")); + if (!groupPrim.IsValid()) { + LOG_ERROR("GroupPrimsCommand::Execute: failed to create group: " + m_groupPath.GetString()); + return; + } + + m_movedNames.clear(); + + for (const SdfPath& srcPath : m_srcPaths) { + UsdPrim src = m_stage->GetPrimAtPath(srcPath); + if (!src.IsValid()) { + LOG_WARNING("GroupPrimsCommand::Execute: prim not found (skipping): " + srcPath.GetString()); + m_movedNames.emplace_back(srcPath.GetName()); + continue; + } + + // Compute a collision-free name inside the group. + std::string name = UniqueNameUnder(m_stage, m_groupPath, srcPath.GetName()); + m_movedNames.push_back(name); + + UsdPrim gPrim = m_stage->GetPrimAtPath(m_groupPath); + UsdNamespaceEditor editor(m_stage); + editor.ReparentPrim(src, gPrim, TfToken(name)); + std::string whyNot; + if (!editor.CanApplyEdits(&whyNot)) { + LOG_ERROR("GroupPrimsCommand::Execute: cannot reparent " + srcPath.GetString() + ": " + whyNot); + continue; + } + if (!editor.ApplyEdits()) { + LOG_ERROR("GroupPrimsCommand::Execute: ApplyEdits failed for " + srcPath.GetString()); + } + } +} + +void GroupPrimsCommand::Undo() { + if (m_movedNames.size() != m_srcPaths.size()) { + LOG_ERROR("GroupPrimsCommand::Undo: state mismatch, cannot undo safely"); + return; + } + + // Reparent children back in reverse order. + for (int i = (int)m_srcPaths.size() - 1; i >= 0; --i) { + SdfPath movedPath = m_groupPath.AppendChild(TfToken(m_movedNames[i])); + UsdPrim moved = m_stage->GetPrimAtPath(movedPath); + if (!moved.IsValid()) { + LOG_WARNING("GroupPrimsCommand::Undo: prim not in group (skipping): " + movedPath.GetString()); + continue; + } + + SdfPath origParentPath = m_srcPaths[i].GetParentPath(); + UsdPrim origParent = GetParentPrim(m_stage, origParentPath); + if (!origParent.IsValid()) { + LOG_ERROR("GroupPrimsCommand::Undo: original parent not found: " + origParentPath.GetString()); + continue; + } + + UsdNamespaceEditor editor(m_stage); + editor.ReparentPrim(moved, origParent, TfToken(m_srcPaths[i].GetName())); + std::string whyNot; + if (!editor.CanApplyEdits(&whyNot)) { + LOG_ERROR("GroupPrimsCommand::Undo: cannot reparent back " + movedPath.GetString() + ": " + whyNot); + continue; + } + if (!editor.ApplyEdits()) { + LOG_ERROR("GroupPrimsCommand::Undo: ApplyEdits failed for " + movedPath.GetString()); + } + } + + // Remove the (now empty) group prim. + m_stage->RemovePrim(m_groupPath); +} + +} // namespace UsdLayerManager diff --git a/src/core/commands/GroupPrimsCommand.h b/src/core/commands/GroupPrimsCommand.h new file mode 100644 index 0000000..38cc43a --- /dev/null +++ b/src/core/commands/GroupPrimsCommand.h @@ -0,0 +1,32 @@ +#pragma once + +#include "../CommandHistory.h" +#include +#include +#include +#include + +namespace UsdLayerManager { + +/// Creates a new Xform group prim and reparents the given prims under it. +/// The group path must be pre-computed (collision-free) by the caller. +/// Undo reparents each child back to its original parent and removes the group. +class GroupPrimsCommand : public ICommand { +public: + GroupPrimsCommand(pxr::UsdStageRefPtr stage, + const std::vector& srcPaths, + const pxr::SdfPath& groupPath); + + void Execute() override; + void Undo() override; + std::string GetDescription() const override { return m_description; } + +private: + pxr::UsdStageRefPtr m_stage; + pxr::SdfPath m_groupPath; + std::vector m_srcPaths; ///< original prim paths (before group) + std::vector m_movedNames; ///< actual names used inside group (set in Execute) + std::string m_description; +}; + +} // namespace UsdLayerManager diff --git a/src/core/commands/RenamePrimCommand.cpp b/src/core/commands/RenamePrimCommand.cpp new file mode 100644 index 0000000..14fbb1f --- /dev/null +++ b/src/core/commands/RenamePrimCommand.cpp @@ -0,0 +1,59 @@ +#include "RenamePrimCommand.h" +#include "../../utils/Logger.h" +#include +#include +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace UsdLayerManager { + +RenamePrimCommand::RenamePrimCommand(UsdStageRefPtr stage, + const SdfPath& primPath, + const std::string& newName) + : m_stage(stage) + , m_oldPath(primPath) + , m_oldName(primPath.GetName()) + , m_newName(newName) + , m_description("Rename " + primPath.GetName() + " \xe2\x86\x92 " + newName) +{ + m_newPath = primPath.GetParentPath().AppendChild(TfToken(newName)); +} + +void RenamePrimCommand::Execute() { + UsdPrim prim = m_stage->GetPrimAtPath(m_oldPath); + if (!prim.IsValid()) { + LOG_ERROR("RenamePrimCommand::Execute: prim not found: " + m_oldPath.GetString()); + return; + } + UsdNamespaceEditor editor(m_stage); + editor.RenamePrim(prim, TfToken(m_newName)); + std::string whyNot; + if (!editor.CanApplyEdits(&whyNot)) { + LOG_ERROR("RenamePrimCommand::Execute: cannot rename " + m_oldPath.GetString() + ": " + whyNot); + return; + } + if (!editor.ApplyEdits()) { + LOG_ERROR("RenamePrimCommand::Execute: ApplyEdits failed for " + m_oldPath.GetString()); + } +} + +void RenamePrimCommand::Undo() { + UsdPrim prim = m_stage->GetPrimAtPath(m_newPath); + if (!prim.IsValid()) { + LOG_ERROR("RenamePrimCommand::Undo: prim not found: " + m_newPath.GetString()); + return; + } + UsdNamespaceEditor editor(m_stage); + editor.RenamePrim(prim, TfToken(m_oldName)); + std::string whyNot; + if (!editor.CanApplyEdits(&whyNot)) { + LOG_ERROR("RenamePrimCommand::Undo: cannot rename back " + m_newPath.GetString() + ": " + whyNot); + return; + } + if (!editor.ApplyEdits()) { + LOG_ERROR("RenamePrimCommand::Undo: ApplyEdits failed for " + m_newPath.GetString()); + } +} + +} // namespace UsdLayerManager diff --git a/src/core/commands/RenamePrimCommand.h b/src/core/commands/RenamePrimCommand.h new file mode 100644 index 0000000..e27b532 --- /dev/null +++ b/src/core/commands/RenamePrimCommand.h @@ -0,0 +1,31 @@ +#pragma once + +#include "../CommandHistory.h" +#include +#include +#include + +namespace UsdLayerManager { + +/// Renames a USD prim via UsdNamespaceEditor, updating all composition arcs. +/// Undo restores the original name. +class RenamePrimCommand : public ICommand { +public: + RenamePrimCommand(pxr::UsdStageRefPtr stage, + const pxr::SdfPath& primPath, + const std::string& newName); + + void Execute() override; + void Undo() override; + std::string GetDescription() const override { return m_description; } + +private: + pxr::UsdStageRefPtr m_stage; + pxr::SdfPath m_oldPath; ///< path before rename + pxr::SdfPath m_newPath; ///< path after rename (set in Execute) + std::string m_oldName; + std::string m_newName; + std::string m_description; +}; + +} // namespace UsdLayerManager diff --git a/src/core/commands/ReparentPrimCommand.cpp b/src/core/commands/ReparentPrimCommand.cpp new file mode 100644 index 0000000..0c9bf6b --- /dev/null +++ b/src/core/commands/ReparentPrimCommand.cpp @@ -0,0 +1,79 @@ +#include "ReparentPrimCommand.h" +#include "../../utils/Logger.h" +#include +#include +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace UsdLayerManager { + +ReparentPrimCommand::ReparentPrimCommand(UsdStageRefPtr stage, + const SdfPath& srcPath, + const SdfPath& dstParentPath, + const std::string& finalName) + : m_stage(stage) + , m_srcPath(srcPath) + , m_origParentPath(srcPath.GetParentPath()) + , m_origName(srcPath.GetName()) + , m_dstParentPath(dstParentPath) + , m_finalName(finalName) + , m_description("Reparent " + srcPath.GetString() + " \xe2\x86\x92 " + + dstParentPath.GetString() + "/" + finalName) +{ + m_actualNewPath = dstParentPath.AppendChild(TfToken(finalName)); +} + +static UsdPrim GetParentPrim(UsdStageRefPtr stage, const SdfPath& parentPath) { + if (parentPath == SdfPath("/") || parentPath == SdfPath::AbsoluteRootPath()) + return stage->GetPseudoRoot(); + return stage->GetPrimAtPath(parentPath); +} + +void ReparentPrimCommand::Execute() { + UsdPrim src = m_stage->GetPrimAtPath(m_srcPath); + if (!src.IsValid()) { + LOG_ERROR("ReparentPrimCommand::Execute: source prim not found: " + m_srcPath.GetString()); + return; + } + UsdPrim dst = GetParentPrim(m_stage, m_dstParentPath); + if (!dst.IsValid()) { + LOG_ERROR("ReparentPrimCommand::Execute: destination parent not found: " + m_dstParentPath.GetString()); + return; + } + UsdNamespaceEditor editor(m_stage); + editor.ReparentPrim(src, dst, TfToken(m_finalName)); + std::string whyNot; + if (!editor.CanApplyEdits(&whyNot)) { + LOG_ERROR("ReparentPrimCommand::Execute: cannot reparent " + m_srcPath.GetString() + ": " + whyNot); + return; + } + if (!editor.ApplyEdits()) { + LOG_ERROR("ReparentPrimCommand::Execute: ApplyEdits failed"); + } +} + +void ReparentPrimCommand::Undo() { + UsdPrim moved = m_stage->GetPrimAtPath(m_actualNewPath); + if (!moved.IsValid()) { + LOG_ERROR("ReparentPrimCommand::Undo: moved prim not found: " + m_actualNewPath.GetString()); + return; + } + UsdPrim origParent = GetParentPrim(m_stage, m_origParentPath); + if (!origParent.IsValid()) { + LOG_ERROR("ReparentPrimCommand::Undo: original parent not found: " + m_origParentPath.GetString()); + return; + } + UsdNamespaceEditor editor(m_stage); + editor.ReparentPrim(moved, origParent, TfToken(m_origName)); + std::string whyNot; + if (!editor.CanApplyEdits(&whyNot)) { + LOG_ERROR("ReparentPrimCommand::Undo: cannot reparent back " + m_actualNewPath.GetString() + ": " + whyNot); + return; + } + if (!editor.ApplyEdits()) { + LOG_ERROR("ReparentPrimCommand::Undo: ApplyEdits failed"); + } +} + +} // namespace UsdLayerManager diff --git a/src/core/commands/ReparentPrimCommand.h b/src/core/commands/ReparentPrimCommand.h new file mode 100644 index 0000000..e56765d --- /dev/null +++ b/src/core/commands/ReparentPrimCommand.h @@ -0,0 +1,35 @@ +#pragma once + +#include "../CommandHistory.h" +#include +#include +#include + +namespace UsdLayerManager { + +/// Reparents a USD prim under a new parent via UsdNamespaceEditor. +/// Supports undo by reparenting back to the original parent with the original name. +/// Pass SdfPath("/") as dstParentPath to reparent to the stage root. +class ReparentPrimCommand : public ICommand { +public: + ReparentPrimCommand(pxr::UsdStageRefPtr stage, + const pxr::SdfPath& srcPath, + const pxr::SdfPath& dstParentPath, + const std::string& finalName); + + void Execute() override; + void Undo() override; + std::string GetDescription() const override { return m_description; } + +private: + pxr::UsdStageRefPtr m_stage; + pxr::SdfPath m_srcPath; ///< original path before reparent + pxr::SdfPath m_origParentPath; ///< parent path before reparent + std::string m_origName; ///< prim name before reparent + pxr::SdfPath m_dstParentPath; ///< target parent path + std::string m_finalName; ///< name inside new parent (collision-free) + pxr::SdfPath m_actualNewPath; ///< full path after Execute (set in Execute) + std::string m_description; +}; + +} // namespace UsdLayerManager diff --git a/src/ui/SceneHierarchyPanel.cpp b/src/ui/SceneHierarchyPanel.cpp index e90d4ca..60ce015 100644 --- a/src/ui/SceneHierarchyPanel.cpp +++ b/src/ui/SceneHierarchyPanel.cpp @@ -5,6 +5,9 @@ #include "../core/commands/DeletePrimCommand.h" #include "../core/commands/AddReferenceCommand.h" #include "../core/commands/ReplaceReferenceCommand.h" +#include "../core/commands/RenamePrimCommand.h" +#include "../core/commands/ReparentPrimCommand.h" +#include "../core/commands/GroupPrimsCommand.h" #include #include #include @@ -118,12 +121,88 @@ Icon SceneHierarchyPanel::GetPrimTypeIconEnum(const UsdPrim& prim) const { return Icon::CircleDot; } +SdfPath SceneHierarchyPanel::FindUniqueChildPath(const SdfPath& parent, + const std::string& baseName) { + std::string name = baseName; + int n = 1; + while (m_stage->GetPrimAtPath(parent.AppendChild(TfToken(name))).IsValid()) + name = baseName + "_" + std::to_string(n++); + return parent.AppendChild(TfToken(name)); +} + +void SceneHierarchyPanel::HandleKeyboardShortcuts() { + if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) return; + // Don't steal keys while any InputText (rename, arc modal, etc.) is active. + if (ImGui::GetIO().WantTextInput) return; + + // F2 — start inline rename on the primary selection. + if (ImGui::IsKeyPressed(ImGuiKey_F2, false) && !m_primarySdfPath.IsEmpty()) { + UsdPrim prim = m_stage->GetPrimAtPath(m_primarySdfPath); + if (prim.IsValid()) { + m_renamingPath = m_primarySdfPath; + std::string name = prim.GetName().GetString(); + strncpy(m_renameBuf, name.c_str(), sizeof(m_renameBuf) - 1); + m_renameBuf[sizeof(m_renameBuf) - 1] = '\0'; + m_renameJustStarted = true; + m_scrollToSelected = true; // ensure the prim is visible + } + } + + // Ctrl+G — group selected prims under a new Xform. + if (ImGui::GetIO().KeyCtrl && + ImGui::IsKeyPressed(ImGuiKey_G, false) && + !m_selectedPaths.empty() && m_commandHistory && m_stage) { + + std::vector srcs; + for (const auto& s : m_selectedPaths) + srcs.emplace_back(s); + + // Filter out paths that are descendants of other selected paths + // (they'll move with their ancestor, so grouping them separately is wrong). + std::vector filtered; + for (const SdfPath& p : srcs) { + bool hasAncestorInSet = false; + for (const SdfPath& other : srcs) { + if (other != p && p.HasPrefix(other)) { + hasAncestorInSet = true; + break; + } + } + if (!hasAncestorInSet) + filtered.push_back(p); + } + if (filtered.empty()) return; + + // Compute common parent of the filtered set. + SdfPath commonParent = filtered[0].GetParentPath(); + for (const SdfPath& p : filtered) { + while (!p.HasPrefix(commonParent)) + commonParent = commonParent.GetParentPath(); + } + + SdfPath groupPath = FindUniqueChildPath(commonParent, "group1"); + m_commandHistory->Push(std::make_unique(m_stage, filtered, groupPath)); + SetSelectedPathFromClick(groupPath.GetString()); + } + + // Delete — open the remove-prim confirmation modal. + if (ImGui::IsKeyPressed(ImGuiKey_Delete, false) && !m_primarySdfPath.IsEmpty()) { + auto rootLayer = m_stage->GetRootLayer(); + if (rootLayer && !!rootLayer->GetPrimAtPath(m_primarySdfPath)) { + m_pendingRemovePrimPath = m_primarySdfPath; + m_showRemovePrimConfirm = true; + } + } +} + void SceneHierarchyPanel::Render() { if (!m_stage) { ImGui::TextDisabled("No stage loaded"); return; } + HandleKeyboardShortcuts(); + // ── Edit-target layer switcher ──────────────────────────────────── { struct LayerEntry { SdfLayerHandle layer; std::string label; }; @@ -222,6 +301,25 @@ void SceneHierarchyPanel::Render() { ImGui::EndTable(); } + // ── Stage-root drop zone ─────────────────────────────────────────── + // Invisible strip below the tree: drop here to reparent a prim to root. + ImGui::InvisibleButton("##rootdrop", ImVec2(-1.0f, 8.0f)); + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("USD_PRIM_PATH")) { + SdfPath srcPath(static_cast(payload->Data)); + // Only reparent if not already a root prim. + if (m_commandHistory && srcPath.GetPathElementCount() > 1) { + std::string name = FindUniqueChildPath(SdfPath("/"), srcPath.GetName()).GetName(); + m_commandHistory->Push(std::make_unique( + m_stage, srcPath, SdfPath("/"), name)); + SetSelectedPathFromClick(SdfPath("/").AppendChild(TfToken(name)).GetString()); + } + } + ImGui::EndDragDropTarget(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Drop here to reparent to stage root"); + // Deselect when left-clicking on blank space (no prim item hovered). if (ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows) && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && @@ -490,45 +588,111 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) { pushedColor = true; } - ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | - ImGuiTreeNodeFlags_OpenOnDoubleClick | - ImGuiTreeNodeFlags_SpanAllColumns; // ← key: full-row item rect - if (isSelected) flags |= ImGuiTreeNodeFlags_Selected; - if (!hasChildren) flags |= ImGuiTreeNodeFlags_Leaf | - ImGuiTreeNodeFlags_NoTreePushOnOpen; + bool isRenaming = (m_renamingPath == primPath); - bool open = ImGui::TreeNodeEx(displayName.c_str(), flags); + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | + ImGuiTreeNodeFlags_SpanAllColumns; // full-row item rect + if (!isRenaming) flags |= ImGuiTreeNodeFlags_OpenOnDoubleClick; + if (isSelected) flags |= ImGuiTreeNodeFlags_Selected; + if (!hasChildren) flags |= ImGuiTreeNodeFlags_Leaf | + ImGuiTreeNodeFlags_NoTreePushOnOpen; - if (pushedColor) - ImGui::PopStyleColor(); + bool open = false; + if (isRenaming) { + // Render just the arrow (hidden label) so we keep indentation and open/close, + // then overlay an InputText inline for the prim name. + open = ImGui::TreeNodeEx("##renaming", flags); + if (pushedColor) + ImGui::PopStyleColor(); + ImGui::SameLine(); + ImGui::SetNextItemWidth(-1.0f); + if (m_renameJustStarted) { + ImGui::SetKeyboardFocusHere(); + m_renameJustStarted = false; + } + bool commit = ImGui::InputText("##rename_input", m_renameBuf, sizeof(m_renameBuf), + ImGuiInputTextFlags_EnterReturnsTrue | + ImGuiInputTextFlags_AutoSelectAll); + bool canceled = ImGui::IsItemDeactivated() && !commit; + if (commit && m_renameBuf[0] != '\0') { + std::string newName = SanitizeUsdName(m_renameBuf); + if (!newName.empty() && newName != displayName && m_commandHistory) { + m_commandHistory->Push(std::make_unique( + m_stage, primPath, newName)); + SdfPath newPath = primPath.GetParentPath().AppendChild(TfToken(newName)); + SetSelectedPathFromClick(newPath.GetString()); + } + m_renamingPath = SdfPath(); + } + if (canceled) + m_renamingPath = SdfPath(); + } else { + open = ImGui::TreeNodeEx(displayName.c_str(), flags); + if (pushedColor) + ImGui::PopStyleColor(); + } - // ── Scroll-to-selection (now reliable: SpanAllColumns gives correct row rect) ── + // ── Scroll-to-selection (SpanAllColumns gives correct full-row rect) ──── if (m_scrollToSelected && primStr == m_primarySelectedPath) { ImGui::SetScrollHereY(0.5f); m_scrollToSelected = false; } - // Selection on click (not on toggle arrow). - if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) - SetSelectedPathFromClick(primStr); + if (!isRenaming) { + // Selection on click (not on toggle arrow). + if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) + SetSelectedPathFromClick(primStr); - // Tooltip. - if (ImGui::IsItemHovered()) { - std::string tip = "Type: " + (typeName.empty() ? "(unknown)" : typeName) + - "\nPath: " + primStr + - "\nActive: " + (isActive ? "Yes" : "No"); - if (isImageable) - tip += std::string("\nVisibility: ") + (isInvisible ? "Invisible" : "Visible"); - if (hasRefs) - tip += "\nHas references"; - if (m_selectedPaths.size() > 1) - tip += "\n\n" + std::to_string(m_selectedPaths.size()) + " prims selected"; - ImGui::SetTooltip("%s", tip.c_str()); + // Double-click to start inline rename. + if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + m_renamingPath = primPath; + strncpy(m_renameBuf, displayName.c_str(), sizeof(m_renameBuf) - 1); + m_renameBuf[sizeof(m_renameBuf) - 1] = '\0'; + m_renameJustStarted = true; + } + + // Tooltip. + if (ImGui::IsItemHovered()) { + std::string tip = "Type: " + (typeName.empty() ? "(unknown)" : typeName) + + "\nPath: " + primStr + + "\nActive: " + (isActive ? "Yes" : "No"); + if (isImageable) + tip += std::string("\nVisibility: ") + (isInvisible ? "Invisible" : "Visible"); + if (hasRefs) + tip += "\nHas references"; + if (m_selectedPaths.size() > 1) + tip += "\n\n" + std::to_string(m_selectedPaths.size()) + " prims selected"; + ImGui::SetTooltip("%s", tip.c_str()); + } + + // ── Drag-and-drop source ───────────────────────────────────────── + if (ImGui::BeginDragDropSource()) { + ImGui::SetDragDropPayload("USD_PRIM_PATH", + primStr.c_str(), primStr.size() + 1, + ImGuiCond_Once); + ImGui::Text("Move: %s", displayName.c_str()); + ImGui::EndDragDropSource(); + } + + // ── Drag-and-drop target ───────────────────────────────────────── + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("USD_PRIM_PATH")) { + SdfPath src(static_cast(payload->Data)); + // Guard: don't reparent onto self or onto a descendant. + if (src != primPath && !primPath.HasPrefix(src) && m_commandHistory) { + std::string name = FindUniqueChildPath(primPath, src.GetName()).GetName(); + m_commandHistory->Push(std::make_unique( + m_stage, src, primPath, name)); + SetSelectedPathFromClick(primPath.AppendChild(TfToken(name)).GetString()); + } + } + ImGui::EndDragDropTarget(); + } + + // Context menu (must follow the last widget = the tree node). + RenderContextMenu(prim); } - // Context menu (must follow the last widget = the tree node). - RenderContextMenu(prim); - // ── Col 1: Prim-type icon ─────────────────────────────────────────────── ImGui::TableNextColumn(); { @@ -653,6 +817,43 @@ void SceneHierarchyPanel::RenderContextMenu(const UsdPrim& prim) { ImGui::Separator(); + // ── Rename / Group ──────────────────────────────────────────────────── + if (ImGui::MenuItem("Rename", "F2")) { + m_renamingPath = prim.GetPath(); + std::string name = prim.GetName().GetString(); + strncpy(m_renameBuf, name.c_str(), sizeof(m_renameBuf) - 1); + m_renameBuf[sizeof(m_renameBuf) - 1] = '\0'; + m_renameJustStarted = true; + m_scrollToSelected = true; + } + if (ImGui::MenuItem("Group", "Ctrl+G")) { + std::vector srcs; + if (m_selectedPaths.count(prim.GetPath().GetString())) + for (const auto& s : m_selectedPaths) srcs.emplace_back(s); + else + srcs.push_back(prim.GetPath()); + if (!srcs.empty() && m_commandHistory && m_stage) { + // Filter descendants + std::vector filtered; + for (const SdfPath& p : srcs) { + bool hasAncestor = false; + for (const SdfPath& other : srcs) + if (other != p && p.HasPrefix(other)) { hasAncestor = true; break; } + if (!hasAncestor) filtered.push_back(p); + } + if (!filtered.empty()) { + SdfPath commonParent = filtered[0].GetParentPath(); + for (const SdfPath& p : filtered) + while (!p.HasPrefix(commonParent)) commonParent = commonParent.GetParentPath(); + SdfPath groupPath = FindUniqueChildPath(commonParent, "group1"); + m_commandHistory->Push(std::make_unique(m_stage, filtered, groupPath)); + SetSelectedPathFromClick(groupPath.GetString()); + } + } + } + + ImGui::Separator(); + bool hasChildren = !prim.GetChildren().empty(); if (ImGui::MenuItem("Expand Children", nullptr, false, hasChildren)) { ImGui::GetStateStorage()->SetInt(ImGui::GetID(prim.GetPath().GetText()), 1); diff --git a/src/ui/SceneHierarchyPanel.h b/src/ui/SceneHierarchyPanel.h index 42785cc..8a2059a 100644 --- a/src/ui/SceneHierarchyPanel.h +++ b/src/ui/SceneHierarchyPanel.h @@ -114,6 +114,14 @@ private: std::string m_arcModalVarSetName; char m_arcModalBuf[512] = {}; bool m_openArcModal = false; + + /// Inline rename state. + SdfPath m_renamingPath; ///< non-empty while renaming + char m_renameBuf[256] = {}; + bool m_renameJustStarted = false; + + void HandleKeyboardShortcuts(); + SdfPath FindUniqueChildPath(const SdfPath& parent, const std::string& baseName); }; } // namespace UsdLayerManager