Add Maya-style prim editing to SceneHierarchyPanel
- Inline rename: double-click prim label or F2 to edit name in-place; Enter commits, click-away or Esc cancels (undo-able) - Drag-and-drop reparent: drag any prim onto another to make it a child; invisible root drop zone below tree to reparent to stage root (undo-able) - Ctrl+G group: creates Xform group at common parent, reparents selection under it; filters descendant duplication; selects new group (undo-able) - Delete key shortcut: opens existing remove-prim modal when panel focused - Context menu: Rename (F2) and Group (Ctrl+G) items added - Three new undo-able commands using UsdNamespaceEditor (USD 25.05): RenamePrimCommand, ReparentPrimCommand, GroupPrimsCommand Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
#include "GroupPrimsCommand.h"
|
||||
#include "../../utils/Logger.h"
|
||||
#include <pxr/usd/usd/namespaceEditor.h>
|
||||
#include <pxr/usd/usd/prim.h>
|
||||
#include <pxr/base/tf/token.h>
|
||||
|
||||
PXR_NAMESPACE_USING_DIRECTIVE
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
GroupPrimsCommand::GroupPrimsCommand(UsdStageRefPtr stage,
|
||||
const std::vector<SdfPath>& 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
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "../CommandHistory.h"
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<pxr::SdfPath>& 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<pxr::SdfPath> m_srcPaths; ///< original prim paths (before group)
|
||||
std::vector<std::string> m_movedNames; ///< actual names used inside group (set in Execute)
|
||||
std::string m_description;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,59 @@
|
||||
#include "RenamePrimCommand.h"
|
||||
#include "../../utils/Logger.h"
|
||||
#include <pxr/usd/usd/namespaceEditor.h>
|
||||
#include <pxr/usd/usd/prim.h>
|
||||
#include <pxr/base/tf/token.h>
|
||||
|
||||
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
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "../CommandHistory.h"
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
#include <string>
|
||||
|
||||
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
|
||||
@@ -0,0 +1,79 @@
|
||||
#include "ReparentPrimCommand.h"
|
||||
#include "../../utils/Logger.h"
|
||||
#include <pxr/usd/usd/namespaceEditor.h>
|
||||
#include <pxr/usd/usd/prim.h>
|
||||
#include <pxr/base/tf/token.h>
|
||||
|
||||
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
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "../CommandHistory.h"
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
#include <string>
|
||||
|
||||
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
|
||||
+229
-28
@@ -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 <pxr/usd/usd/prim.h>
|
||||
#include <pxr/usd/usd/primRange.h>
|
||||
#include <pxr/usd/usd/references.h>
|
||||
@@ -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<SdfPath> 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<SdfPath> 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<GroupPrimsCommand>(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<const char*>(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<ReparentPrimCommand>(
|
||||
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<RenamePrimCommand>(
|
||||
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<const char*>(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<ReparentPrimCommand>(
|
||||
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<SdfPath> 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<SdfPath> 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<GroupPrimsCommand>(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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user