Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions cmake/modules/FindUFE.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -277,3 +277,12 @@ if(UFE_INCLUDE_DIR AND EXISTS "${UFE_INCLUDE_DIR}/ufe/colorManagementHandler.h")
set(UFE_HAS_COLOR_MANAGEMENT_HANDLER TRUE CACHE INTERNAL "cmHandler")
message(STATUS "Maya has UFE Color Management handler")
endif()

set(UFE_BATCH_OPS_HAS_DUPLICATE_TO_TARGET FALSE CACHE INTERNAL "ufeBatchOpsHasDuplicateToTarget")
if(UFE_INCLUDE_DIR AND EXISTS "${UFE_INCLUDE_DIR}/ufe/batchOpsHandler.h")
file(STRINGS ${UFE_INCLUDE_DIR}/ufe/batchOpsHandler.h UFE_HAS_API REGEX "kDstParentPath")
if(UFE_HAS_API)
set(UFE_BATCH_OPS_HAS_DUPLICATE_TO_TARGET TRUE CACHE INTERNAL "ufeBatchOpsHasDuplicateToTarget")
message(STATUS "Maya has UFE BatchOpsHandler's duplicateSelectionCmd_ interface with dstParentPath option")
endif()
endif()
7 changes: 7 additions & 0 deletions lib/mayaUsd/ufe/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,13 @@ if (UFE_CAMERAHANDLER_HAS_FINDALL)
)
endif()

if (UFE_BATCH_OPS_HAS_DUPLICATE_TO_TARGET)
target_compile_definitions(${PROJECT_NAME}
PRIVATE
UFE_BATCH_OPS_HAS_DUPLICATE_TO_TARGET=1
)
endif()

if(UFE_CAMERAHANDLER_HAS_FINDALL)
target_sources(${PROJECT_NAME}
PRIVATE
Expand Down
25 changes: 25 additions & 0 deletions lib/mayaUsd/ufe/UsdBatchOpsHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
#include "UsdBatchOpsHandler.h"

#include <mayaUsd/ufe/UsdUndoDuplicateSelectionCommand.h>
#include <mayaUsd/ufe/Utils.h>

#include <ufe/pathString.h>

#ifdef UFE_BATCH_OPS_HAS_DUPLICATE_TO_TARGET
#include <usdUfe/ufe/UsdUndoDuplicateSelectionCommand.h>
#endif

namespace MAYAUSD_NS_DEF {
namespace ufe {
Expand All @@ -41,6 +48,24 @@ Ufe::SelectionUndoableCommand::Ptr UsdBatchOpsHandler::duplicateSelectionCmd_(
const Ufe::Selection& selection,
const Ufe::ValueDictionary& duplicateOptions)
{
#ifdef UFE_BATCH_OPS_HAS_DUPLICATE_TO_TARGET
// Duplicating to a specific parent item.
const auto itParent = duplicateOptions.find(kDstParentPath);
if (itParent != duplicateOptions.end()) {
if (!itParent->second.isType<std::string>()) {
return nullptr;
}
const auto parentPath = Ufe::PathString::path(itParent->second.get<std::string>());
const auto parentItem = downcast(Ufe::Hierarchy::createItem(parentPath));
if (!parentItem) {
return nullptr;
}

return UsdUfe::UsdUndoDuplicateSelectionCommand::create(selection, parentItem);
}
#endif

// Duplicating in place.
return UsdUndoDuplicateSelectionCommand::create(selection, duplicateOptions);
}

Expand Down
15 changes: 15 additions & 0 deletions lib/usdUfe/ufe/UsdUndoDuplicateSelectionCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
#include <usdUfe/ufe/Utils.h>
#include <usdUfe/undo/UsdUndoBlock.h>

#include <ufe/hierarchy.h>

namespace USDUFE_NS_DEF {

// Ensure that UsdUndoDuplicateSelectionCommand is properly setup.
Expand Down Expand Up @@ -236,4 +238,17 @@ void UsdUndoDuplicateSelectionCommand::undo() { _undoableItem.undo(); }

void UsdUndoDuplicateSelectionCommand::redo() { _undoableItem.redo(); }

Ufe::SceneItem::Ptr UsdUndoDuplicateSelectionCommand::targetItem(const Ufe::Path& sourcePath) const
{
const auto it = std::find_if(
_duplicatedItemsMap.begin(), _duplicatedItemsMap.end(), [&sourcePath](const auto& pair) {
return pair.first->path() == sourcePath;
});
if (it == _duplicatedItemsMap.end()) {
return nullptr;
}

return Ufe::Hierarchy::createItem(it->second->path());

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recreate the item in case it went stale.

I tried changing _duplicatedItemsMap to just store paths to prevent items from going stale in the first place, but that caused issues with the clipboard. If this command is used to duplicate |stage1|stageShape1,/mtl/add1 to the clipboard, the UFE path of the duplicated item will just be /add1 -- it's at the root of the clipboard stage.

Ufe::Hierarchy::createItem() is not able to create an item from /add1, so the clipboard relies on the actual item being stored and returned by UsdUndoDuplicateSelectionCommand::getDuplicatedItemsMap().

}

} // namespace USDUFE_NS_DEF
4 changes: 3 additions & 1 deletion lib/usdUfe/ufe/UsdUndoDuplicateSelectionCommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ namespace USDUFE_NS_DEF {
using DuplicatedItemsMap = std::unordered_map<UsdSceneItem::Ptr, UsdSceneItem::Ptr>;

//! \brief UsdUndoDuplicateSelectionCommand
class USDUFE_PUBLIC UsdUndoDuplicateSelectionCommand : public Ufe::UndoableCommand
class USDUFE_PUBLIC UsdUndoDuplicateSelectionCommand : public Ufe::SelectionUndoableCommand
{
public:
using Ptr = std::shared_ptr<UsdUndoDuplicateSelectionCommand>;
Expand All @@ -59,6 +59,8 @@ class USDUFE_PUBLIC UsdUndoDuplicateSelectionCommand : public Ufe::UndoableComma
void redo() override;
UFE_V4(std::string commandString() const override { return "DuplicateSelection"; })

Ufe::SceneItem::Ptr targetItem(const Ufe::Path& sourcePath) const override;

//! Retrieve all the duplicated items.
Ufe::SceneItemList targetItems() const;

Expand Down
1 change: 1 addition & 0 deletions test/lib/ufe/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ foreach(script ${TEST_SCRIPT_FILES})
"UFE_SCENEITEM_HAS_METADATA=${UFE_SCENEITEM_HAS_METADATA}"
"HAS_LOOKDEVXUSD=${BUILD_LOOKDEVXUSD_LIBRARY}"
"UFE_HAS_UNSIGNED_INT=${UFE_HAS_UNSIGNED_INT}"
"UFE_BATCH_OPS_HAS_DUPLICATE_TO_TARGET=${UFE_BATCH_OPS_HAS_DUPLICATE_TO_TARGET}"
)

# Add a ctest label to these tests for easy filtering.
Expand Down
101 changes: 101 additions & 0 deletions test/lib/ufe/testBatchOpsHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,107 @@ def testDuplicatedNodeGraph(self):
self.assertTrue(dNgPrim.HasProperty("inputs:file4:varname"))
self.assertTrue(dNgPrim.HasProperty("outputs:baseColor"))

@unittest.skipUnless(os.getenv('UFE_BATCH_OPS_HAS_DUPLICATE_TO_TARGET', 'FALSE') == 'TRUE', 'Test requires kDstParentPath option.')
def testDuplicatedToTarget_invalidDstParentPath(self):
testFile = testUtils.getTestScene('MaterialX', 'BatchOpsTestScene.usda')
shapeNode,shapeStage = mayaUtils.createProxyFromFile(testFile)

materialItem = ufeUtils.createUfeSceneItem(shapeNode, '/mtl/ss3SG')
childItem = ufe.Hierarchy.createItem(materialItem.path() + 'ss3')

# Get the USD BatchOpsHandler
kDstParentPath = ufe.BatchOpsHandler.kDstParentPath
batchOpsHandler = ufe.RunTimeMgr.instance().batchOpsHandler(materialItem.runTimeId())
self.assertIsNotNone(batchOpsHandler)

# Calling the method with a non-existent path should return None.
nonExistentPath = materialItem.path() + "nonExistentItem"
nonExistentPathString = ufe.PathString.string(nonExistentPath)
self.assertIsNone(batchOpsHandler.duplicateSelectionCmd_(ufe.Selection(childItem), {kDstParentPath: nonExistentPathString}))

# Calling the method with an incorrect type should return None.
self.assertIsNone(batchOpsHandler.duplicateSelectionCmd_(ufe.Selection(childItem), {kDstParentPath: False}))

# Calling the method with an empty selection should return None.
validParentPathString = ufe.PathString.string(materialItem.path())
self.assertIsNone(batchOpsHandler.duplicateSelectionCmd_(ufe.Selection(), {kDstParentPath: validParentPathString}))

@unittest.skipUnless(os.getenv('UFE_BATCH_OPS_HAS_DUPLICATE_TO_TARGET', 'FALSE') == 'TRUE', 'Test requires kDstParentPath option.')
def testDuplicateToTarget_validDstParentPath(self):
"""Test duplicating a selection of items from multiple locations to a target location.
Verify that connections between the items are maintained but external connections are
removed."""
testFile = testUtils.getTestScene('MaterialX', 'BatchOpsTestScene.usda')
shapeNode,shapeStage = mayaUtils.createProxyFromFile(testFile)

materialItem1 = ufeUtils.createUfeSceneItem(shapeNode, '/mtl/ss3SG')
materialItem2 = ufeUtils.createUfeSceneItem(shapeNode, '/mtl/ss4SG')

# Items to duplicate:
ss1 = ufe.Hierarchy.createItem(materialItem1.path() + 'ss3')
compound1 = ufe.Hierarchy.createItem(materialItem1.path() + 'MayaNG_ss3SG')
ss2 = ufe.Hierarchy.createItem(materialItem2.path() + 'ss4')
compound2 = ufe.Hierarchy.createItem(materialItem2.path() + 'MayaNG_ss4SG')
compound2Child1 = ufe.Hierarchy.createItem(compound2.path() + 'place2dTexture4')
compound2Child2 = ufe.Hierarchy.createItem(compound2.path() + 'file4')
selection = ufe.Selection([ss1, compound1, ss2, compound2Child1, compound2Child2])

# Duplicate this selection to `compound2`.
dstParentPathString = ufe.PathString.string(compound2.path())
duplicateCmd = ufe.BatchOpsHandler.duplicateSelectionCmd(selection, {ufe.BatchOpsHandler.kDstParentPath: dstParentPathString})
self.assertIsNotNone(duplicateCmd)
numChildrenBefore = len(ufe.Hierarchy.hierarchy(compound2).children())
duplicateCmd.execute()
numChildrenAfter = len(ufe.Hierarchy.hierarchy(compound2).children())

# Delete the materials and undo to make all scene items go stale. Undoing and redoing the
# duplicate command should still work correctly afterwards.
deleteMaterial1Cmd = ufe.SceneItemOps.sceneItemOps(materialItem1).deleteItemCmdNoExecute()
deleteMaterial1Cmd.execute()
deleteMaterial1Cmd.undo()
deleteMaterial2Cmd = ufe.SceneItemOps.sceneItemOps(materialItem2).deleteItemCmdNoExecute()
deleteMaterial2Cmd.execute()
deleteMaterial2Cmd.undo()

# Undo should delete the duplicated items and redo should recreate them.
compound2Hierarchy = ufe.Hierarchy.hierarchy(ufe.Hierarchy.createItem(compound2.path()))
duplicateCmd.undo()
self.assertEqual(numChildrenBefore, len(compound2Hierarchy.children()))
duplicateCmd.redo()
self.assertEqual(numChildrenAfter, len(compound2Hierarchy.children()))

# Verify that all duplicated items are children of the specified parent item.
self.assertEqual(compound2.path(), duplicateCmd.targetItem(ss1.path()).path().pop())
self.assertEqual(compound2.path(), duplicateCmd.targetItem(compound1.path()).path().pop())
self.assertEqual(compound2.path(), duplicateCmd.targetItem(ss2.path()).path().pop())
self.assertEqual(compound2.path(), duplicateCmd.targetItem(compound2Child1.path()).path().pop())
self.assertEqual(compound2.path(), duplicateCmd.targetItem(compound2Child2.path()).path().pop())

# Verify that the connections of the duplicated items are correct. Connections between the
# items should be maintained but external connections should be removed. The following
# connections are expected between the duplicated items:
# - compound1 -> ss1
# - compound1/someChild -> compound1
# - compound2Child1 -> compound2Child2
ch = ufe.RunTimeMgr.instance().connectionHandler(materialItem1.runTimeId())

ss1Connections = ch.sourceConnections(duplicateCmd.targetItem(ss1.path())).allConnections()
self.assertEqual(len(ss1Connections), 1)
self.assertEqual(ss1Connections[0].src.path, duplicateCmd.targetItem(compound1.path()).path())

compound1Connections = ch.sourceConnections(duplicateCmd.targetItem(compound1.path())).allConnections()
self.assertEqual(len(compound1Connections), 1)
self.assertEqual(compound1Connections[0].src.path.pop(), duplicateCmd.targetItem(compound1.path()).path())

ss2Connections = ch.sourceConnections(duplicateCmd.targetItem(ss2.path())).allConnections()
self.assertEqual(len(ss2Connections), 0)

child1Connections = ch.sourceConnections(duplicateCmd.targetItem(compound2Child1.path())).allConnections()
self.assertEqual(len(child1Connections), 0)

child2Connections = ch.sourceConnections(duplicateCmd.targetItem(compound2Child2.path())).allConnections()
self.assertEqual(len(child2Connections), 1)
self.assertEqual(child2Connections[0].src.path, duplicateCmd.targetItem(compound2Child1.path()).path())

if __name__ == '__main__':
unittest.main(verbosity=2)