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:
2026-06-27 19:44:55 +08:00
parent 88ae8ccefa
commit b31ca69d4f
8 changed files with 586 additions and 28 deletions
+113
View File
@@ -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
+32
View File
@@ -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
+59
View File
@@ -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
+31
View File
@@ -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
+79
View File
@@ -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
+35
View File
@@ -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