From 7a0ee452c3cbdc59940e3df092a39bab9707ca51 Mon Sep 17 00:00:00 2001 From: Jannik Frohne Date: Thu, 27 Apr 2023 14:09:54 +0200 Subject: [PATCH] MAYA-129059 - Add command to create stages. Add an undoable command that creates a stage with a new layer. The new command is a C++ implementation of the existing mayaUsd_createStageWithNewLayer.py Python script. The C++ command will: - enable the creation of stages from within the C++ code. - enable the creation of stages deep in the Maya hierarchy. - wrap stage creation into an Ufe::UndoableCommand so that it can be undone by UFE clients. This commit also adds a Python binding for the stage creation command, which is used in mayaUsd_createStageWithNewLayer.py to replace the current implementation. --- lib/mayaUsd/ufe/CMakeLists.txt | 2 + .../UsdUndoCreateStageWithNewLayerCommand.cpp | 211 ++++++++++++++++++ .../UsdUndoCreateStageWithNewLayerCommand.h | 74 ++++++ lib/mayaUsd/ufe/wrapUtils.cpp | 29 +++ .../mayaUsd_createStageWithNewLayer.py | 17 +- test/lib/ufe/testPythonWrappers.py | 36 +++ 6 files changed, 364 insertions(+), 5 deletions(-) create mode 100644 lib/mayaUsd/ufe/UsdUndoCreateStageWithNewLayerCommand.cpp create mode 100644 lib/mayaUsd/ufe/UsdUndoCreateStageWithNewLayerCommand.h diff --git a/lib/mayaUsd/ufe/CMakeLists.txt b/lib/mayaUsd/ufe/CMakeLists.txt index 47bfdbac6a..8cd6b734f4 100644 --- a/lib/mayaUsd/ufe/CMakeLists.txt +++ b/lib/mayaUsd/ufe/CMakeLists.txt @@ -167,6 +167,7 @@ if(CMAKE_UFE_V4_FEATURES_AVAILABLE) UsdUndoAttributesCommands.cpp UsdTransform3dRead.cpp UsdUndoConnectionCommands.cpp + UsdUndoCreateStageWithNewLayerCommand.cpp ) endif() @@ -302,6 +303,7 @@ if(CMAKE_UFE_V4_FEATURES_AVAILABLE) UsdUndoAttributesCommands.h UsdTransform3dRead.h UsdUndoConnectionCommands.h + UsdUndoCreateStageWithNewLayerCommand.h ) endif() diff --git a/lib/mayaUsd/ufe/UsdUndoCreateStageWithNewLayerCommand.cpp b/lib/mayaUsd/ufe/UsdUndoCreateStageWithNewLayerCommand.cpp new file mode 100644 index 0000000000..f5d4fd17ac --- /dev/null +++ b/lib/mayaUsd/ufe/UsdUndoCreateStageWithNewLayerCommand.cpp @@ -0,0 +1,211 @@ +// +// Copyright 2023 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "UsdUndoCreateStageWithNewLayerCommand.h" + +#include +#include +#include + +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace MAYAUSD_NS_DEF { +namespace ufe { + +UsdUndoCreateStageWithNewLayerCommand::UsdUndoCreateStageWithNewLayerCommand( + const Ufe::SceneItem::Ptr& parentItem) + : _parentItem(nullptr) + , _insertedChild(nullptr) + , _createTransformDagMod(MDagModifierUndoItem::create("Create transform")) + , _createProxyShapeDagMod(MDagModifierUndoItem::create("Create Stage with new Layer")) + , _success(false) +{ + if (!TF_VERIFY(parentItem)) + return; + + _parentItem = parentItem; +} + +UsdUndoCreateStageWithNewLayerCommand::~UsdUndoCreateStageWithNewLayerCommand() { } + +UsdUndoCreateStageWithNewLayerCommand::Ptr +UsdUndoCreateStageWithNewLayerCommand::create(const Ufe::SceneItem::Ptr& parentItem) +{ + if (!parentItem) { + return nullptr; + } + + return std::make_shared(parentItem); +} + +Ufe::SceneItem::Ptr UsdUndoCreateStageWithNewLayerCommand::sceneItem() const +{ + return _insertedChild; +} + +void UsdUndoCreateStageWithNewLayerCommand::execute() +{ + if (!_parentItem) { + return; + } + + bool createTransformSuccess = false; + try { + // Get a MObject from the parent scene item. + // Note: If and only if the parent is the world node, MDagPath::transform() will set status + // to kInvalidParameter. In this case MObject::kNullObj is returned, which is a valid parent + // object. Thus, kInvalidParameter will not be treated as a failure. + MStatus status; + MDagPath parentDagPath = MayaUsd::ufe::ufeToDagPath(_parentItem->path()); + MObject parentObject = parentDagPath.transform(&status); + if (status != MStatus::kInvalidParameter && MFAIL(status)) { + throw std::runtime_error(""); + } + + // Create a transform node. + // Note: It would be possible to create the transform and the proxy shape in one doIt() call + // of a single MDagModifier. However, doing so causes notifications to be sent in a + // different order, which triggers a `TF_VERIFY(g_StageMap.isDirty())` in + // StagesSubject::onStageSet(). Using a separate MDagModifier to create the transform seems + // more robust and avoids triggering the TF_VERIFY. + MObject transformObj; + transformObj = _createTransformDagMod.createNode("transform", parentObject, &status); + if (MFAIL(status)) { + throw std::runtime_error(""); + } + TF_VERIFY(!transformObj.isNull()); + status = _createTransformDagMod.doIt(); + if (MFAIL(status)) { + throw std::runtime_error(""); + } + createTransformSuccess = true; + + // Create a proxy shape. + MObject proxyShape; + proxyShape = _createProxyShapeDagMod.createNode("mayaUsdProxyShape", transformObj, &status); + if (MFAIL(status)) { + throw std::runtime_error(""); + } + TF_VERIFY(!proxyShape.isNull()); + + // Rename the transform and the proxy shape. + // Note: The transform is renamed twice. The first rename operation renames it from its + // default name "transform1" to "stage1". The number-suffix will be automatically + // incremented if necessary. The second rename operation renames it from "stageX" to + // "stage1". This doesn't do anything for the transform itself but it will adjust the + // number-suffix of the proxy shape according to the suffix of the transform, because they + // now share the common prefix "stage". + status = _createProxyShapeDagMod.renameNode(proxyShape, "stageShape1"); + if (MFAIL(status)) { + throw std::runtime_error(""); + } + status = _createProxyShapeDagMod.renameNode(transformObj, "stage1"); + if (MFAIL(status)) { + throw std::runtime_error(""); + } + status = _createProxyShapeDagMod.renameNode(transformObj, "stage1"); + if (MFAIL(status)) { + throw std::runtime_error(""); + } + + // Get the global `time1` object and its `outTime` attribute. + MSelectionList selection; + selection.add("time1"); + MObject time1; + status = selection.getDependNode(0, time1); + if (MFAIL(status)) { + throw std::runtime_error(""); + } + MFnDependencyNode time1DepNodeFn(time1, &status); + if (MFAIL(status)) { + throw std::runtime_error(""); + } + MObject time1OutTimeAttr = time1DepNodeFn.attribute("outTime", &status); + if (MFAIL(status)) { + throw std::runtime_error(""); + } + + // Get the `time` attribute of the newly created mayaUsdProxyShape. + MDagPath proxyShapeDagPath; + status = MDagPath::getAPathTo(proxyShape, proxyShapeDagPath); + if (MFAIL(status)) { + throw std::runtime_error(""); + } + MFnDependencyNode proxyShapeDepNodeFn(proxyShapeDagPath.node(), &status); + if (MFAIL(status)) { + throw std::runtime_error(""); + } + MObject proxyShapeTimeAttr = proxyShapeDepNodeFn.attribute("time", &status); + if (MFAIL(status)) { + throw std::runtime_error(""); + } + + // Connect `time1.outTime` to `proxyShapde.time`. + status = _createProxyShapeDagMod.connect( + time1, time1OutTimeAttr, proxyShape, proxyShapeTimeAttr); + if (MFAIL(status)) { + throw std::runtime_error(""); + } + + // Execute the operations. + status = _createProxyShapeDagMod.doIt(); + if (MFAIL(status)) { + throw std::runtime_error(""); + } + _success = true; + + // Create a UFE scene item for the newly created mayaUsdProxyShape. + Ufe::Path proxyShapeUfePath = MayaUsd::ufe::dagPathToUfe(proxyShapeDagPath); + _insertedChild = Ufe::Hierarchy::createItem(proxyShapeUfePath); + + // Refresh the cache of the stage map. + // When creating the proxy shape, the stage map gets dirtied and cleaned. Afterwards, the + // proxy shape is renamed. The stage map does not observe the Maya data model, so renaming + // does not dirty the stage map again. Thus, the cache is in an invalid state, where it + // contains the path of the proxy shape before it was renamed. Calling getProxyShape() + // refreshes the cache. See comments within UsdStageMap::proxyShape() for more details. + getProxyShape(proxyShapeUfePath); + } catch (const std::exception&) { + if (createTransformSuccess) { + _createTransformDagMod.undoIt(); + } + } +} + +void UsdUndoCreateStageWithNewLayerCommand::undo() +{ + if (_success) { + _createProxyShapeDagMod.undoIt(); + _createTransformDagMod.undoIt(); + } +} + +void UsdUndoCreateStageWithNewLayerCommand::redo() +{ + if (_success) { + _createTransformDagMod.doIt(); + _createProxyShapeDagMod.doIt(); + + // Refresh the cache of the stage map. + if (_insertedChild) { + getProxyShape(_insertedChild->path()); + } + } +} + +} // namespace ufe +} // namespace MAYAUSD_NS_DEF diff --git a/lib/mayaUsd/ufe/UsdUndoCreateStageWithNewLayerCommand.h b/lib/mayaUsd/ufe/UsdUndoCreateStageWithNewLayerCommand.h new file mode 100644 index 0000000000..00f01c8f1d --- /dev/null +++ b/lib/mayaUsd/ufe/UsdUndoCreateStageWithNewLayerCommand.h @@ -0,0 +1,74 @@ +// +// Copyright 2023 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef MAYAUSD_UFE_CREATESTAGEWITHNEWLAYERCOMMAND_H +#define MAYAUSD_UFE_CREATESTAGEWITHNEWLAYERCOMMAND_H + +#include + +#include +#include + +namespace MAYAUSD_NS_DEF { +namespace ufe { + +//! \brief This command is used to create a new empty stage in memory. +class MAYAUSD_CORE_PUBLIC UsdUndoCreateStageWithNewLayerCommand + : public Ufe::SceneItemResultUndoableCommand +{ +public: + typedef std::shared_ptr Ptr; + + UsdUndoCreateStageWithNewLayerCommand(const Ufe::SceneItem::Ptr& parentItem); + ~UsdUndoCreateStageWithNewLayerCommand() override; + + // Delete the copy/move constructors assignment operators. + UsdUndoCreateStageWithNewLayerCommand(const UsdUndoCreateStageWithNewLayerCommand&) = delete; + UsdUndoCreateStageWithNewLayerCommand& operator=(const UsdUndoCreateStageWithNewLayerCommand&) + = delete; + UsdUndoCreateStageWithNewLayerCommand(UsdUndoCreateStageWithNewLayerCommand&&) = delete; + UsdUndoCreateStageWithNewLayerCommand& operator=(UsdUndoCreateStageWithNewLayerCommand&&) + = delete; + + //! Create a UsdUndoCreateStageWithNewLayerCommand. Executing this command should produce the + //! following: + //! - Proxyshape + //! - Stage + //! - Session Layer + //! - Anonymous Root Layer (this is set as the target layer) + //! Since the proxy shape does not have a USD file associated (in the .filePath attribute), the + //! proxy shape base will create an empty stage in memory. This will create the session and root + //! layer as well. + static UsdUndoCreateStageWithNewLayerCommand::Ptr create(const Ufe::SceneItem::Ptr& parentItem); + + Ufe::SceneItem::Ptr sceneItem() const override; + + void execute() override; + void undo() override; + void redo() override; + +private: + Ufe::SceneItem::Ptr _parentItem; + Ufe::SceneItem::Ptr _insertedChild; + + MDagModifier& _createTransformDagMod; + MDagModifier& _createProxyShapeDagMod; + bool _success; +}; // UsdUndoCreateStageWithNewLayerCommand + +} // namespace ufe +} // namespace MAYAUSD_NS_DEF + +#endif // MAYAUSD_UFE_CREATESTAGEWITHNEWLAYERCOMMAND_H diff --git a/lib/mayaUsd/ufe/wrapUtils.cpp b/lib/mayaUsd/ufe/wrapUtils.cpp index 7deced3856..ab2f89566c 100644 --- a/lib/mayaUsd/ufe/wrapUtils.cpp +++ b/lib/mayaUsd/ufe/wrapUtils.cpp @@ -31,6 +31,12 @@ #include #endif +#ifdef UFE_V4_FEATURES_AVAILABLE +#include + +#include +#endif + #include #include @@ -239,6 +245,25 @@ PXR_NS::TfTokenVector getProxyShapePurposes(const std::string& ufePathString) return ufe::getProxyShapePurposes(path); } +#ifdef UFE_V4_FEATURES_AVAILABLE +std::string createStageWithNewLayer(const std::string& parentPathString) +{ + auto parentPath = Ufe::PathString::path(parentPathString); + auto parent = Ufe::Hierarchy::createItem(parentPath); + auto command = MayaUsd::ufe::UsdUndoCreateStageWithNewLayerCommand::create(parent); + if (!command) { + return ""; + } + + Ufe::UndoableCommandMgr::instance().executeCmd(command); + if (!command->sceneItem()) { + return ""; + } + + return Ufe::PathString::string(command->sceneItem()->path()); +} +#endif + void wrapUtils() { #ifdef UFE_V2_FEATURES_AVAILABLE @@ -247,6 +272,10 @@ void wrapUtils() def("getNodeTypeFromRawItem", getNodeTypeFromRawItem); #endif +#ifdef UFE_V4_FEATURES_AVAILABLE + def("createStageWithNewLayer", createStageWithNewLayer); +#endif + // Because mayaUsd and UFE have incompatible Python bindings that do not // know about each other (provided by Boost Python and pybind11, // respectively), we cannot pass in or return UFE objects such as Ufe::Path diff --git a/plugin/adsk/scripts/mayaUsd_createStageWithNewLayer.py b/plugin/adsk/scripts/mayaUsd_createStageWithNewLayer.py index 39f659faff..045abe358a 100644 --- a/plugin/adsk/scripts/mayaUsd_createStageWithNewLayer.py +++ b/plugin/adsk/scripts/mayaUsd_createStageWithNewLayer.py @@ -14,6 +14,7 @@ # import maya.cmds as cmds +import mayaUsd def createStageWithNewLayer(): """For use in aggregating USD (Assembly) and creating layer structures (Layout) @@ -29,8 +30,14 @@ def createStageWithNewLayer(): # Simply create a proxy shape. Since it does not have a USD file associated # (in the .filePath attribute), the proxy shape base will create an empty # stage in memory. This will create the session and root layer as well. - shapeNode = cmds.createNode('mayaUsdProxyShape', skipSelect=True, name='stageShape1') - cmds.connectAttr('time1.outTime', shapeNode+'.time') - cmds.select(shapeNode, replace=True) - fullPath = cmds.ls(shapeNode, long=True) - return fullPath[0] + if hasattr(mayaUsd, 'ufe') and hasattr(mayaUsd.ufe, 'createStageWithNewLayer'): + shapeNode = mayaUsd.ufe.createStageWithNewLayer('|world') + cmds.select(shapeNode, replace=True) + return shapeNode + else: + shapeNode = cmds.createNode('mayaUsdProxyShape', skipSelect=True, name='stageShape1') + cmds.connectAttr('time1.outTime', shapeNode+'.time') + cmds.select(shapeNode, replace=True) + fullPath = cmds.ls(shapeNode, long=True) + return fullPath[0] + diff --git a/test/lib/ufe/testPythonWrappers.py b/test/lib/ufe/testPythonWrappers.py index cc35e13415..fe465157ed 100644 --- a/test/lib/ufe/testPythonWrappers.py +++ b/test/lib/ufe/testPythonWrappers.py @@ -55,6 +55,7 @@ def setUp(self): def testWrappers(self): ''' Verify the python wrappers.''' + cmds.file(new=True, force=True) # Create empty stage and add a prim. import mayaUsd_createStageWithNewLayer @@ -121,6 +122,7 @@ def testWrappers(self): # investigated as needed. @unittest.skipUnless((mayaUtils.mayaMajorVersion() == 2023) or mayaUtils.previewReleaseVersion() >= 139, 'Only supported in Maya 2023 or greater.') def testGetAllStages(self): + cmds.file(new=True, force=True) # Create two stages. import mayaUsd_createStageWithNewLayer @@ -143,5 +145,39 @@ def testGetAllStages(self): self.assertEqual(len(mayaUsd.ufe.getAllStages()), 1) + @unittest.skipUnless(ufeUtils.ufeFeatureSetVersion() >= 4, 'Test for UFE v4 or later') + def testCreateStageWithNewLayerBinding(self): + cmds.file(new=True, force=True) + + def verifyProxyShape(proxyShapePathString): + # Verify that we got a proxy shape object. + nodeType = cmds.nodeType(proxyShapePathString) + self.assertEqual('mayaUsdProxyShape', nodeType) + + # Verify that the shape node is connected to time. + self.assertTrue(cmds.isConnected('time1.outTime', proxyShapePathString+'.time')) + + # Create a proxy shape under the world node. + proxy1PathString = mayaUsd.ufe.createStageWithNewLayer('|world') + self.assertEqual('|stage1|stageShape1', proxy1PathString) + verifyProxyShape(proxy1PathString) + self.assertEqual(len(mayaUsd.ufe.getAllStages()), 1) + cmds.undo() + self.assertEqual(len(mayaUsd.ufe.getAllStages()), 0) + cmds.redo() + self.assertEqual(len(mayaUsd.ufe.getAllStages()), 1) + + # Create a proxy shape under a transform. + cmds.createNode('transform', skipSelect=True, name='transform1') + transformPathString = '|transform1' + proxy2PathString = mayaUsd.ufe.createStageWithNewLayer(transformPathString) + self.assertEqual('|transform1|stage1|stageShape1', proxy2PathString) + verifyProxyShape(proxy2PathString) + self.assertEqual(len(mayaUsd.ufe.getAllStages()), 2) + cmds.undo() + self.assertEqual(len(mayaUsd.ufe.getAllStages()), 1) + cmds.redo() + self.assertEqual(len(mayaUsd.ufe.getAllStages()), 2) + if __name__ == '__main__': unittest.main(verbosity=2)