From defb1098999645a1085b658a32778e1e3158be79 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 command enables the creation of stages from within the C++ code, which will be required by future work (MAYA-128151) and might also be generally useful. 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 | 72 ++++++ lib/mayaUsd/ufe/wrapUtils.cpp | 33 +++ .../mayaUsd_createStageWithNewLayer.py | 17 +- 5 files changed, 330 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 cc2e04048c..c01b01656d 100644 --- a/lib/mayaUsd/ufe/CMakeLists.txt +++ b/lib/mayaUsd/ufe/CMakeLists.txt @@ -160,6 +160,7 @@ if(CMAKE_UFE_V4_FEATURES_AVAILABLE) UsdUndoAttributesCommands.cpp UsdTransform3dRead.cpp UsdUndoConnectionCommands.cpp + UsdUndoCreateStageWithNewLayerCommand.cpp ) endif() @@ -295,6 +296,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..2a58661f3f --- /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 + +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace MAYAUSD_NS_DEF { +namespace ufe { + +extern UsdStageMap g_StageMap; + +UsdUndoCreateStageWithNewLayerCommand::UsdUndoCreateStageWithNewLayerCommand( + const Ufe::SceneItem::Ptr& parentItem) + : _parentItem(nullptr) + , _insertedChild(nullptr) + , _createTransformDagMod(MDagModifierUndoItem::create("Create transform")) + , _createProxyShapeDagMod(MDagModifierUndoItem::create("Create Stage with new Layer")) + , _createTransformSuccess(false) + , _createProxyShapeSuccess(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; + } + + // 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)) { + return; + } + + // 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)) { + return; + } + TF_VERIFY(!transformObj.isNull()); + status = _createTransformDagMod.doIt(); + if (MFAIL(status)) { + return; + } + _createTransformSuccess = true; + + // Create a proxy shape. + MObject proxyShape; + proxyShape = _createProxyShapeDagMod.createNode("mayaUsdProxyShape", transformObj, &status); + if (MFAIL(status)) { + return; + } + 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)) { + return; + } + status = _createProxyShapeDagMod.renameNode(transformObj, "stage1"); + if (MFAIL(status)) { + return; + } + status = _createProxyShapeDagMod.renameNode(transformObj, "stage1"); + if (MFAIL(status)) { + return; + } + + // Get the global `time1` object and its `outTime` attribute. + MSelectionList selection; + selection.add("time1"); + MObject time1; + status = selection.getDependNode(0, time1); + if (MFAIL(status)) { + return; + } + MFnDependencyNode time1DepNodeFn(time1, &status); + if (MFAIL(status)) { + return; + } + MObject time1OutTimeAttr = time1DepNodeFn.attribute("outTime", &status); + if (MFAIL(status)) { + return; + } + + // Get the `time` attribute of the newly created mayaUsdProxyShape. + MDagPath proxyShapeDagPath = MDagPath::getAPathTo(proxyShape, &status); + if (MFAIL(status)) { + return; + } + MFnDependencyNode proxyShapeDepNodeFn(proxyShapeDagPath.node(), &status); + if (MFAIL(status)) { + return; + } + MObject proxyShapeTimeAttr = proxyShapeDepNodeFn.attribute("time", &status); + if (MFAIL(status)) { + return; + } + + // Connect `time1.outTime` to `proxyShapde.time`. + status + = _createProxyShapeDagMod.connect(time1, time1OutTimeAttr, proxyShape, proxyShapeTimeAttr); + if (MFAIL(status)) { + return; + } + + // Execute the operations. + status = _createProxyShapeDagMod.doIt(); + if (MFAIL(status)) { + return; + } + _createProxyShapeSuccess = 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 UsdStageMap::proxyShape() refreshes + // the cache. See comments within UsdStageMap::proxyShape() for more details. + g_StageMap.proxyShape(proxyShapeUfePath); +} + +void UsdUndoCreateStageWithNewLayerCommand::undo() +{ + if (_createProxyShapeSuccess) { + _createProxyShapeDagMod.undoIt(); + } + + if (_createTransformSuccess) { + _createTransformDagMod.undoIt(); + } +} + +void UsdUndoCreateStageWithNewLayerCommand::redo() +{ + if (_createTransformSuccess) { + _createTransformDagMod.doIt(); + } + + if (_createProxyShapeSuccess) { + _createProxyShapeDagMod.doIt(); + } + + // Refresh the cache of the stage map. + if (_insertedChild) { + g_StageMap.proxyShape(_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..4cefe17bae --- /dev/null +++ b/lib/mayaUsd/ufe/UsdUndoCreateStageWithNewLayerCommand.h @@ -0,0 +1,72 @@ +// +// 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. +// +#pragma once + +#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 _createTransformSuccess; + bool _createProxyShapeSuccess; +}; // UsdUndoCreateStageWithNewLayerCommand + +} // namespace ufe +} // namespace MAYAUSD_NS_DEF diff --git a/lib/mayaUsd/ufe/wrapUtils.cpp b/lib/mayaUsd/ufe/wrapUtils.cpp index 7deced3856..f163da9989 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,29 @@ 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); + if (!parent) { + return ""; + } + + 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 +276,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] +