From 87357246fe286da04cf2ce1bef9819cca5ba3ba8 Mon Sep 17 00:00:00 2001 From: Pierre Baillargeon Date: Wed, 22 Mar 2023 15:35:52 -0400 Subject: [PATCH] MAYA-126040 improve preventing edits - Add documentations about edit routing. - Make attribute editing not throw exception for now to avoid possibly destabilizing Maya. - Instead, attribute commands return null command when edits are prevented. - Make the commands use the pre-delcraed USD tokens (code cleanup) - Make the visibility command always apply routing by moving routing code in its constructor. - Add more unit test for edit routing, testing more commands. - Add more unit tests for command prevention --- doc/EditRouting.md | 245 +++++++++++++ lib/mayaUsd/ufe/UsdAttributeHolder.cpp | 14 +- lib/mayaUsd/ufe/UsdUndoDuplicateCommand.cpp | 3 +- lib/mayaUsd/ufe/UsdUndoInsertChildCommand.cpp | 3 +- lib/mayaUsd/ufe/UsdUndoVisibleCommand.cpp | 21 +- lib/mayaUsd/ufe/UsdUndoVisibleCommand.h | 5 +- test/lib/ufe/CMakeLists.txt | 1 + test/lib/ufe/testEditRouting.py | 338 ++++++++++++++++++ test/lib/ufe/testVisibilityCmd.py | 135 ------- 9 files changed, 608 insertions(+), 157 deletions(-) create mode 100644 doc/EditRouting.md create mode 100644 test/lib/ufe/testEditRouting.py diff --git a/doc/EditRouting.md b/doc/EditRouting.md new file mode 100644 index 0000000000..adcd443d6d --- /dev/null +++ b/doc/EditRouting.md @@ -0,0 +1,245 @@ +# Edit Routing + +## What is edit routing + +By default, when a Maya user modifies USD data, the modifications are written +to the current edit target. That is, to the current targeted layer. The current +layer is selected in the Layer Manager window in Maya. + +Edit routing is a mechanism to select which USD layer will receive edits. +When a routable edit is about to happen, the Maya USD plugin can temporarily +change the targeted layer so that the modifications are written to a specific +layer, instead of the current target layer. + +The mechanism allows users to write code (scripts or plugins) to handle the +routing. The script receives information about which USD prim is about to be +change and the name of the operation that is about to modify that prim. In +the case of attribute changes, the name of the attribute is also provided. + +Given these informations, the script can choose a specific layer among the +available layers. It returns the layer it wants to be targeted. If the script +does not wish to select a specific layer, it just returns nothing and the +current targeted layer will be used. + +## What can be routed + +Currently, edit routing is divided in two categories: commands and attributes. + +For commands, the edit routing is called it receives an operation name that +corresponds to the specific command to be routed. Only a subset of commands can +be routed, but this subset is expected to grow. The operations that can be +routed are named: + +- duplicate +- parent (including parenting and grouping) +- visibility +- mayaReferencePush + +For attributes, any modifications to the attribute value, including metadata, +can be routed. The edit routing is called with an 'attribute' operation name +and receives the name of the attribute that is about to be modified. + +## API for edit routing + +An edit router can be written in C++ or Python. The principle is the same in +both cases, so we will describe the Python version. The C++ version is similar. + +The edit router is a function that receives two arguments. The arguments are +both dictionaries (dict in Python, VtDictionary in C++). Each is filled with +data indexed by USD tokens (TfToken): + +- Context: the input context of the routing. +- Routing: the output data of the routing. + +In theory, each edit routing operation could fill the context differently +and expect different data in the output dictionary. In practice many operations +share the same inputs and outputs. Currently, the operations can be divided in +three categories: + +- Simple commands +- Attributes +- Maya references + +The following sections describe the input and output of each category. Each +input or output is listed with its token name and the data that it contains. + +### Simple commands + +Inputs: +- prim: the USD prim (UsdPrim) that is being affected. +- operation: the operation name (TfToken). Either visibility, duplicate or parent. + +Outputs: +- layer: the desired layer ID (text string) or layer handle (SdfLayerHandle). + +On return, if the layer entry is empty, no routing is done and the current edit +target is used. Here is an example of a simple edit router: + +```Python +def routeToSessionLayer(context, routingData): + ''' + Edit router implementation for that routes to the session layer + of the stage that contains the prim. + ''' + prim = context.get('prim') + if prim is None: + print('Prim not in context') + return + + routingData['layer'] = prim.GetStage().GetSessionLayer().identifier + +``` + +### Attributes + +Inputs: +- prim: the USD prim (UsdPrim) that is being affected. +- operation: the operation name (TfToken). Either visibility, duplicate or parent. +- attribute: the attribute name, including its namespace, if any (TfToken). + +Outputs: +- layer: the desired layer ID (text string) or layer handle (SdfLayerHandle). + +On return, if the layer entry is empty, no routing is done and the current edit +target is used. Here is an example of an attribute edit router: + +```Python +def routeAttrToSessionLayer(context, routingData): + ''' + Edit router implementation for 'attribute' operations that routes + to the session layer of the stage that contains the prim. + ''' + prim = context.get('prim') + if prim is None: + print('Prim not in context') + return + + attrName = context.get('attribute') + if attrName != "visibility": + return + + routingData['layer'] = prim.GetStage().GetSessionLayer().identifier +``` + +### Maya references + +The maya reference edit routing is more complex than the other ones. It is +described in the following documentation: [Maya Reference Edit Router](lib/usd/translators/mayaReferenceEditRouter.md). + +## API to register edit routing + +The Maya USD plugin provides C++ and Python functions to register edit routers. +The function is called `registerEditRouter` and takes as arguments the name of +the operation to be routed, as a USD token (TfToken) and the function that will +do the routing. For example, the following Python script routes the `visibility` +operation using a function called `routeToSessionLayer`: + +```Python +import mayaUsd.lib +mayaUsd.lib.registerEditRouter('visibility', routeToSessionLayer) +``` + +## Canceling commands + +It is possible to prevent a command from executing instead of simply routing to +a layer. This is done by raising an exception in the edit router. The command +handles the exception and does not execute. This is how an end-user (or studio) +can block certain types of edits. For example, to prevent artists from modifying +things they should not touch. For instance, a lighting specialist might not be +allowed to move props in a scene. + +For example, to prevent all opertions for which it is registered, one could use +the following Python edit router: + +```Python +def preventCommandEditRouter(context, routingData): + ''' + Edit router that prevents an operation from happening. + ''' + opName = context.get('operation') or 'unknown operation' + raise Exception('Sorry, %s is not permitted' % opName) +``` + +## Persisting the edit routers + +Edit routers must be registered with the MayaUSD plugin each time Maya is +launched. This can be automated via the standard Maya startup scripts. +The user startup script is called `userSetup.mel`. It is located in the +`scripts` folder in the yearly Maya release folder in the user's `Documents` +folder. For example: `Documents\maya\2024\scripts\userSetup.mel`. + +This is a MEL script, so any logic necessary to register a given set of edit +routers can be performed. For example, one can detect that the Maya USD plugin +is loaded and register the custom edit routers like this: + +```MEL +global proc registerUserEditRouters() { + if (`pluginInfo -q -loaded mayaUsdPlugin`) { + python("import userEditRouters; userEditRouters.registerEditRouters()"); + } else { + print("*** Missing Maya USD plugin!!! ***"); + } +} + +scriptJob -permanent -event "NewSceneOpened" "registerUserEditRouters"; +``` + +This requires the Python script that does the registration of edit routers +to exists in the user `site-packages`, located next to the user scripts in +this folder: `Documents\maya\2024\scripts\site-packages`. + +For example, to continue the example given above, the following Python script +could be used: + +```Python +import mayaUsd.lib + +sessionAttributes = set(['visibility', 'radius']) + +def routeToSessionLayer(context, routingData): + ''' + Edit router implementation for that routes to the session layer + of the stage that contains the prim. + ''' + prim = context.get('prim') + if prim is None: + print('Prim not in context') + return + + routingData['layer'] = prim.GetStage().GetSessionLayer().identifier + +def routeAttrToSessionLayer(context, routingData): + ''' + Edit router implementation for 'attribute' operations that routes + to the session layer of the stage that contains the prim. + ''' + prim = context.get('prim') + if prim is None: + print('Prim not in context') + return + + attrName = context.get('attribute') + if attrName not in sessionAttributes: + return + + routingData['layer'] = prim.GetStage().GetSessionLayer().identifier + +def registerAttributeEditRouter(): + ''' + Register an edit router for the 'attribute' operation that routes to + the session layer. + ''' + mayaUsd.lib.registerEditRouter('attribute', routeAttrToSessionLayer) + +def registerVisibilityEditRouter(): + ''' + Register an edit router for the 'visibility' operation that routes to + the session layer. + ''' + mayaUsd.lib.registerEditRouter('visibility', routeToSessionLayer) + +def registerEditRouters(): + registerAttributeEditRouter() + registerVisibilityEditRouter() + +``` \ No newline at end of file diff --git a/lib/mayaUsd/ufe/UsdAttributeHolder.cpp b/lib/mayaUsd/ufe/UsdAttributeHolder.cpp index 30d6169289..3ef0e3a2b6 100644 --- a/lib/mayaUsd/ufe/UsdAttributeHolder.cpp +++ b/lib/mayaUsd/ufe/UsdAttributeHolder.cpp @@ -116,8 +116,18 @@ UsdAttributeHolder::UPtr UsdAttributeHolder::create(const PXR_NS::UsdAttribute& std::string UsdAttributeHolder::isEditAllowedMsg() const { if (isValid()) { - PXR_NS::UsdPrim prim = _usdAttr.GetPrim(); - PXR_NS::SdfLayerHandle layer = getAttrEditRouterLayer(prim, _usdAttr.GetName()); + PXR_NS::UsdPrim prim = _usdAttr.GetPrim(); + + // Edit routing is done by a user-provided implementation that can raise exceptions. + // In particular, they can raise an exception to prevent the execution of the associated + // command. This is directly relevant for this check of allowed edits. + PXR_NS::SdfLayerHandle layer; + try { + layer = getAttrEditRouterLayer(prim, _usdAttr.GetName()); + } catch (std::exception&) { + return "Editing has been prevented by edit router."; + } + PXR_NS::UsdEditContext ctx(prim.GetStage(), layer); std::string errMsg; diff --git a/lib/mayaUsd/ufe/UsdUndoDuplicateCommand.cpp b/lib/mayaUsd/ufe/UsdUndoDuplicateCommand.cpp index 106d20ae50..d8168c2f2f 100644 --- a/lib/mayaUsd/ufe/UsdUndoDuplicateCommand.cpp +++ b/lib/mayaUsd/ufe/UsdUndoDuplicateCommand.cpp @@ -18,6 +18,7 @@ #include "private/UfeNotifGuard.h" #include "private/Utils.h" +#include #include #include #include @@ -58,7 +59,7 @@ UsdUndoDuplicateCommand::UsdUndoDuplicateCommand(const UsdSceneItem::Ptr& srcIte _usdDstPath = parentPrim.GetPath().AppendChild(TfToken(newName)); _srcLayer = MayaUsdUtils::getDefiningLayerAndPath(srcPrim).layer; - _dstLayer = getEditRouterLayer(PXR_NS::TfToken("duplicate"), srcPrim); + _dstLayer = getEditRouterLayer(MayaUsdEditRoutingTokens->RouteDuplicate, srcPrim); } UsdUndoDuplicateCommand::~UsdUndoDuplicateCommand() { } diff --git a/lib/mayaUsd/ufe/UsdUndoInsertChildCommand.cpp b/lib/mayaUsd/ufe/UsdUndoInsertChildCommand.cpp index 6eb62ee303..c667fb1826 100644 --- a/lib/mayaUsd/ufe/UsdUndoInsertChildCommand.cpp +++ b/lib/mayaUsd/ufe/UsdUndoInsertChildCommand.cpp @@ -20,6 +20,7 @@ #include "private/UfeNotifGuard.h" #include "private/Utils.h" +#include #include #include #include @@ -132,7 +133,7 @@ UsdUndoInsertChildCommand::UsdUndoInsertChildCommand( ufe::applyCommandRestriction(parentPrim, "reparent"); _childLayer = childPrim.GetStage()->GetEditTarget().GetLayer(); - _parentLayer = getEditRouterLayer(PXR_NS::TfToken("parent"), parentPrim); + _parentLayer = getEditRouterLayer(MayaUsdEditRoutingTokens->RouteParent, parentPrim); } UsdUndoInsertChildCommand::~UsdUndoInsertChildCommand() { } diff --git a/lib/mayaUsd/ufe/UsdUndoVisibleCommand.cpp b/lib/mayaUsd/ufe/UsdUndoVisibleCommand.cpp index c1b914081b..5e3763c495 100644 --- a/lib/mayaUsd/ufe/UsdUndoVisibleCommand.cpp +++ b/lib/mayaUsd/ufe/UsdUndoVisibleCommand.cpp @@ -15,6 +15,7 @@ // #include "UsdUndoVisibleCommand.h" +#include #include #include #include @@ -24,15 +25,15 @@ namespace MAYAUSD_NS_DEF { namespace ufe { -UsdUndoVisibleCommand::UsdUndoVisibleCommand( - const UsdPrim& prim, - bool vis, - const PXR_NS::SdfLayerHandle& layer) +UsdUndoVisibleCommand::UsdUndoVisibleCommand(const UsdPrim& prim, bool vis) : Ufe::UndoableCommand() , _prim(prim) , _visible(vis) - , _layer(layer) + , _layer(getEditRouterLayer(MayaUsdEditRoutingTokens->RouteVisibility, prim)) { + EditTargetGuard guard(prim, _layer); + UsdGeomImageable primImageable(prim); + enforceAttributeEditAllowed(primImageable.GetVisibilityAttr()); } UsdUndoVisibleCommand::~UsdUndoVisibleCommand() { } @@ -43,15 +44,7 @@ UsdUndoVisibleCommand::Ptr UsdUndoVisibleCommand::create(const UsdPrim& prim, bo return nullptr; } - auto layer = getEditRouterLayer(PXR_NS::TfToken("visibility"), prim); - - UsdGeomImageable primImageable(prim); - - EditTargetGuard guard(prim, layer); - - enforceAttributeEditAllowed(primImageable.GetVisibilityAttr()); - - return std::make_shared(prim, vis, layer); + return std::make_shared(prim, vis); } void UsdUndoVisibleCommand::execute() diff --git a/lib/mayaUsd/ufe/UsdUndoVisibleCommand.h b/lib/mayaUsd/ufe/UsdUndoVisibleCommand.h index 79e2f73be9..0bf209a76b 100644 --- a/lib/mayaUsd/ufe/UsdUndoVisibleCommand.h +++ b/lib/mayaUsd/ufe/UsdUndoVisibleCommand.h @@ -32,10 +32,7 @@ class MAYAUSD_CORE_PUBLIC UsdUndoVisibleCommand : public Ufe::UndoableCommand typedef std::shared_ptr Ptr; // Public for std::make_shared() access, use create() instead. - UsdUndoVisibleCommand( - const PXR_NS::UsdPrim& prim, - bool vis, - const PXR_NS::SdfLayerHandle& layer); + UsdUndoVisibleCommand(const PXR_NS::UsdPrim& prim, bool vis); ~UsdUndoVisibleCommand() override; // Delete the copy/move constructors assignment operators. diff --git a/test/lib/ufe/CMakeLists.txt b/test/lib/ufe/CMakeLists.txt index 659103cffb..ed7a79d428 100644 --- a/test/lib/ufe/CMakeLists.txt +++ b/test/lib/ufe/CMakeLists.txt @@ -25,6 +25,7 @@ if(CMAKE_UFE_V2_FEATURES_AVAILABLE) testComboCmd.py testContextOps.py testDuplicateCmd.py + testEditRouting.py testGroupCmd.py testMoveCmd.py testObject3d.py diff --git a/test/lib/ufe/testEditRouting.py b/test/lib/ufe/testEditRouting.py new file mode 100644 index 0000000000..42332cdfb1 --- /dev/null +++ b/test/lib/ufe/testEditRouting.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python + +import fixturesUtils +from maya import cmds +from maya import standalone +import mayaUsd.ufe +import mayaUtils +import ufe +import unittest +import usdUtils +from pxr import UsdGeom + +def filterUsdStr(usdSceneStr): + '''Remove empty lines and lines starting with pound character.''' + nonBlankLines = filter(None, [l.rstrip() for l in usdSceneStr.splitlines()]) + finalLines = [l for l in nonBlankLines if not l.startswith('#')] + return '\n'.join(finalLines) + +def routeCmdToSessionLayer(context, routingData): + ''' + Edit router for commands, routing to the session layer. + ''' + prim = context.get('prim') + if prim is None: + print('Prim not in context') + return + + routingData['layer'] = prim.GetStage().GetSessionLayer().identifier + +def routeVisibilityAttribute(context, routingData): + ''' + Edit router for attributes, routing to the session layer. + ''' + prim = context.get('prim') + if prim is None: + print('Prim not in context') + return + + attrName = context.get('attribute') + if attrName != UsdGeom.Tokens.visibility: + return + + routingData['layer'] = prim.GetStage().GetSessionLayer().identifier + +def preventCommandRouter(context, routingData): + ''' + Edit router that prevents an operation from happening. + ''' + opName = context.get('operation') or 'operation' + raise Exception('Sorry, %s is not permitted' % opName) + + +class EditRoutingTestCase(unittest.TestCase): + '''Verify the Maya Edit Router for visibility.''' + pluginsLoaded = False + + @classmethod + def setUpClass(cls): + fixturesUtils.readOnlySetUpClass(__file__, loadPlugin=False) + if not cls.pluginsLoaded: + cls.pluginsLoaded = mayaUtils.isMayaUsdPluginLoaded() + + @classmethod + def tearDownClass(cls): + standalone.uninitialize() + + def setUp(self): + self.assertTrue(self.pluginsLoaded) + + cmds.file(new=True, force=True) + import mayaUsd_createStageWithNewLayer + + # Create the following hierarchy: + # + # proxy shape + # |_ A + # |_ B + # + + psPathStr = mayaUsd_createStageWithNewLayer.createStageWithNewLayer() + stage = mayaUsd.lib.GetPrim(psPathStr).GetStage() + stage.DefinePrim('/A', 'Xform') + stage.DefinePrim('/B', 'Xform') + + psPath = ufe.PathString.path(psPathStr) + psPathSegment = psPath.segments[0] + aPath = ufe.Path([psPathSegment, usdUtils.createUfePathSegment('/A')]) + bPath = ufe.Path([psPathSegment, usdUtils.createUfePathSegment('/B')]) + self.a = ufe.Hierarchy.createItem(aPath) + self.b = ufe.Hierarchy.createItem(bPath) + + cmds.select(clear=True) + + def tearDown(self): + # Restore default edit routers. + mayaUsd.lib.restoreAllDefaultEditRouters() + + def _verifyEditRouterForCmd(self, operationName, cmdFunc, verifyFunc): + ''' + Test edit router functionality for the given operation name, using the given command and verifier. + The B xform will be in the global selection and is assumed to be affected by the command. + ''' + + # Get the session layer + prim = mayaUsd.ufe.ufePathToPrim("|stage1|stageShape1,/A") + sessionLayer = prim.GetStage().GetSessionLayer() + + # Check that the session layer is empty + self.assertTrue(sessionLayer.empty) + + # Send visibility edits to the session layer. + mayaUsd.lib.registerEditRouter(operationName, routeCmdToSessionLayer) + + # Select /B + sn = ufe.GlobalSelection.get() + sn.clear() + sn.append(self.b) + + # Affect B via the command + cmdFunc() + + # Check that something was written to the session layer + self.assertIsNotNone(sessionLayer) + + # Verify the command was routed. + verifyFunc(sessionLayer) + + def testEditRouterForVisibilityCmd(self): + ''' + Test edit router functionality for the set-visibility command. + ''' + + def setVisibility(): + cmds.hide() + + def verifyVisibility(sessionLayer): + self.assertIsNotNone(sessionLayer.GetPrimAtPath('/B')) + + # Check that any visibility changes were written to the session layer + self.assertIsNotNone(sessionLayer.GetAttributeAtPath('/B.visibility').default) + + # Check that correct visibility changes were written to the session layer + self.assertEqual(filterUsdStr(sessionLayer.ExportToString()), + 'over "B"\n{\n token visibility = "invisible"\n}') + + self._verifyEditRouterForCmd('visibility', setVisibility, verifyVisibility) + + def testEditRouterForDuplicateCmd(self): + ''' + Test edit router functionality for the duplicate command. + ''' + + def duplicate(): + cmds.duplicate() + + def verifyDuplicate(sessionLayer): + # Check that the duplicated prim was created in the session layer + self.assertIsNotNone(sessionLayer.GetPrimAtPath('/B1')) + self.assertTrue(sessionLayer.GetPrimAtPath('/B1')) + + # Check that correct duplicated prim was written to the session layer + self.assertEqual(filterUsdStr(sessionLayer.ExportToString()), + 'def Xform "B1"\n{\n}') + + self._verifyEditRouterForCmd('duplicate', duplicate, verifyDuplicate) + + def testEditRouterForParentCmd(self): + ''' + Test edit router functionality for the parent command. + ''' + + def group(): + cmds.group() + + def verifyGroup(sessionLayer): + # Check that correct grouped prim was written to the session layer + self.assertEqual(filterUsdStr(sessionLayer.ExportToString()), + 'over "group1"\n{\n def Xform "B"\n {\n }\n}') + + # Check that the grouped prim was created in the session layer + self.assertIsNotNone(sessionLayer.GetPrimAtPath('/group1')) + self.assertTrue(sessionLayer.GetPrimAtPath('/group1')) + self.assertIsNotNone(sessionLayer.GetPrimAtPath('/group1/B')) + self.assertTrue(sessionLayer.GetPrimAtPath('/group1/B')) + + self._verifyEditRouterForCmd('parent', group, verifyGroup) + + def testEditRouterForSetVisibility(self): + ''' + Test edit router for the visibility attribute triggered + via UFE Object3d's setVisibility function. + ''' + + # Get the session layer + prim = mayaUsd.ufe.ufePathToPrim("|stage1|stageShape1,/A") + sessionLayer = prim.GetStage().GetSessionLayer() + + # Check that the session layer is empty + self.assertTrue(sessionLayer.empty) + + # Send visibility edits to the session layer. + mayaUsd.lib.registerEditRouter('attribute', routeVisibilityAttribute) + + # Hide B via the UFE Object3d setVisbility function + object3d = ufe.Object3d.object3d(self.b) + object3d.setVisibility(False) + + # Check that something was written to the session layer + self.assertIsNotNone(sessionLayer) + self.assertFalse(sessionLayer.empty) + self.assertIsNotNone(sessionLayer.GetPrimAtPath('/B')) + + # Check that any visibility changes were written to the session layer + self.assertIsNotNone(sessionLayer.GetAttributeAtPath('/B.visibility').default) + + # Check that correct visibility changes were written to the session layer + self.assertEqual(filterUsdStr(sessionLayer.ExportToString()), + 'over "B"\n{\n token visibility = "invisible"\n}') + + def testEditRouterForAttributeVisibility(self): + ''' + Test edit router for the visibility attribute triggered + via UFE attribute's set function. + ''' + + # Get the session layer + prim = mayaUsd.ufe.ufePathToPrim("|stage1|stageShape1,/A") + sessionLayer = prim.GetStage().GetSessionLayer() + + # Check that the session layer is empty + self.assertTrue(sessionLayer.empty) + + # Send visibility edits to the session layer. + mayaUsd.lib.registerEditRouter('attribute', routeVisibilityAttribute) + + # Hide B via the UFE attribute set function. + attrs = ufe.Attributes.attributes(self.b) + visibilityAttr = attrs.attribute(UsdGeom.Tokens.visibility) + visibilityAttr.set(UsdGeom.Tokens.invisible) + + # Check that something was written to the session layer + self.assertIsNotNone(sessionLayer) + self.assertFalse(sessionLayer.empty) + self.assertIsNotNone(sessionLayer.GetPrimAtPath('/B')) + + # Check that any visibility changes were written to the session layer + self.assertIsNotNone(sessionLayer.GetAttributeAtPath('/B.visibility').default) + + # Check that correct visibility changes were written to the session layer + self.assertEqual(filterUsdStr(sessionLayer.ExportToString()), + 'over "B"\n{\n token visibility = "invisible"\n}') + + # Check we are still allowed to set the attribute without + # explicitly changing the edit target. + try: + self.assertIsNotNone(visibilityAttr.setCmd(UsdGeom.Tokens.invisible)) + except Exception: + self.assertFalse(True, "Should have been able to create a command") + + def _verifyEditRouterPreventingCmd(self, operationName, cmdFunc, verifyFunc): + ''' + Test that an edit router can prevent a command for the given operation name, + using the given command and verifier. + The B xform will be in the global selection and is assumed to be affected by the command. + ''' + # Get the session layer + prim = mayaUsd.ufe.ufePathToPrim("|stage1|stageShape1,/A") + stage = prim.GetStage() + sessionLayer = stage.GetSessionLayer() + + # Check that the session layer is empty + self.assertTrue(sessionLayer.empty) + + # Prevent edits. + mayaUsd.lib.registerEditRouter(operationName, preventCommandRouter) + + # Select /B + sn = ufe.GlobalSelection.get() + sn.clear() + sn.append(self.b) + + # try to affect B via the command, should be prevented + with self.assertRaises(RuntimeError): + cmdFunc() + + # Check that nothing was written to the session layer + self.assertIsNotNone(sessionLayer) + self.assertIsNone(sessionLayer.GetPrimAtPath('/B')) + + # Check that no changes were done in any layer + verifyFunc(stage) + + def testPreventVisibilityCmd(self): + ''' + Test edit router preventing a the visibility command by raising an exception. + ''' + + def hide(): + cmds.hide() + + def verifyNoHide(stage): + # Check that the visibility attribute was not created. + self.assertFalse(stage.GetAttributeAtPath('/B.visibility').HasAuthoredValue()) + + self._verifyEditRouterPreventingCmd('visibility', hide, verifyNoHide) + + @unittest.skipUnless(mayaUtils.mayaMajorVersion() >= 2025, 'Requires Maya fixes for duplicate command error messages only available in Maya 2025 or greater.') + def testPreventDuplicateCmd(self): + ''' + Test edit router preventing the duplicate command by raising an exception. + ''' + + def duplicate(): + cmds.duplicate() + + def verifyNoDuplicate(stage): + # Check that the duplicated prim was not created + self.assertFalse(stage.GetPrimAtPath('/B1')) + + self._verifyEditRouterPreventingCmd('duplicate', duplicate, verifyNoDuplicate) + + def testPreventParentingCmd(self): + ''' + Test edit router preventing the parent operation by raising an exception. + ''' + + def group(): + cmds.group() + + def verifyNoGroup(stage): + # Check that the grouped prim was not created. + self.assertFalse(stage.GetPrimAtPath('/group1')) + + self._verifyEditRouterPreventingCmd('parent', group, verifyNoGroup) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/test/lib/ufe/testVisibilityCmd.py b/test/lib/ufe/testVisibilityCmd.py index 4d84473134..02a7abf8af 100644 --- a/test/lib/ufe/testVisibilityCmd.py +++ b/test/lib/ufe/testVisibilityCmd.py @@ -24,32 +24,6 @@ def getSessionLayer(context, routingData): routingData['layer'] = prim.GetStage().GetSessionLayer().identifier -def preventCommandRouter(context, routingData): - opName = context.get('operation') or 'operation' - raise Exception('Sorry, %s is not permitted' % opName) - -def routerForVisibilityAttribute(context, routingData): - prim = context.get('prim') - if prim is None: - print('Prim not in context') - return - - attrName = context.get('attribute') - if attrName != UsdGeom.Tokens.visibility: - return - - routingData['layer'] = prim.GetStage().GetSessionLayer().identifier - -def createUfePathSegment(usdPath): - """ - Create an UFE path from a given usd path. - Args: - usdPath (str): The usd path to use - Returns : - PathSegment of the given usdPath - """ - return ufe.PathSegment(usdPath, mayaUsd.ufe.getUsdRunTimeId(), '/') - class VisibilityCmdTestCase(unittest.TestCase): '''Verify the Maya Edit Router for visibility.''' pluginsLoaded = False @@ -168,115 +142,6 @@ def testEditRouterForCmdShowHideMultipleSelection(self): # Check visibility was written to the session layer. self.assertEqual(filterUsdStr(sessionLayer.ExportToString()), 'over "A"\n{\n token visibility = "invisible"\n}\nover "B"\n{\n token visibility = "invisible"\n}') - - def testEditRouterForSetVisibility(self): - ''' - Test edit router for the visibility attribute triggered - via UFE Object3d's setVisibility function. - ''' - - # Get the session layer - prim = mayaUsd.ufe.ufePathToPrim("|stage1|stageShape1,/A") - sessionLayer = prim.GetStage().GetSessionLayer() - - # Check that the session layer is empty - self.assertTrue(sessionLayer.empty) - - # Send visibility edits to the session layer. - mayaUsd.lib.registerEditRouter('attribute', routerForVisibilityAttribute) - - # Hide B via the UFE Object3d setVisbility function - object3d = ufe.Object3d.object3d(self.b) - object3d.setVisibility(False) - - # Check that something was written to the session layer - self.assertIsNotNone(sessionLayer) - self.assertFalse(sessionLayer.empty) - self.assertIsNotNone(sessionLayer.GetPrimAtPath('/B')) - - # Check that any visibility changes were written to the session layer - self.assertIsNotNone(sessionLayer.GetAttributeAtPath('/B.visibility').default) - - # Check that correct visibility changes were written to the session layer - self.assertEqual(filterUsdStr(sessionLayer.ExportToString()), - 'over "B"\n{\n token visibility = "invisible"\n}') - - def testEditRouterForAttributeVisibility(self): - ''' - Test edit router for the visibility attribute triggered - via UFE attribute's set function. - ''' - - # Get the session layer - prim = mayaUsd.ufe.ufePathToPrim("|stage1|stageShape1,/A") - sessionLayer = prim.GetStage().GetSessionLayer() - - # Check that the session layer is empty - self.assertTrue(sessionLayer.empty) - - # Send visibility edits to the session layer. - mayaUsd.lib.registerEditRouter('attribute', routerForVisibilityAttribute) - - # Hide B via the UFE attribute set function. - attrs = ufe.Attributes.attributes(self.b) - visibilityAttr = attrs.attribute(UsdGeom.Tokens.visibility) - visibilityAttr.set(UsdGeom.Tokens.invisible) - - # Check that something was written to the session layer - self.assertIsNotNone(sessionLayer) - self.assertFalse(sessionLayer.empty) - self.assertIsNotNone(sessionLayer.GetPrimAtPath('/B')) - - # Check that any visibility changes were written to the session layer - self.assertIsNotNone(sessionLayer.GetAttributeAtPath('/B.visibility').default) - - # Check that correct visibility changes were written to the session layer - self.assertEqual(filterUsdStr(sessionLayer.ExportToString()), - 'over "B"\n{\n token visibility = "invisible"\n}') - - # Check we are still allowed to set the attribute without - # explicitly changing the edit target. - try: - self.assertIsNotNone(visibilityAttr.setCmd(UsdGeom.Tokens.invisible)) - except Exception: - self.assertFalse(True,"Should have been able to create a command") - - def testEditRouterPreventingCmd(self): - ''' - Test edit router preventing a command by raising an exception. - ''' - - # Select /A - sn = ufe.GlobalSelection.get() - sn.clear() - sn.append(self.a) - - # Get the session layer - prim = mayaUsd.ufe.ufePathToPrim("|stage1|stageShape1,/A") - stage = prim.GetStage() - sessionLayer = stage.GetSessionLayer() - - # Check that the session layer is empty - self.assertTrue(sessionLayer.empty) - - # Prevent visibility edits. - mayaUsd.lib.registerEditRouter('visibility', preventCommandRouter) - - # Select /B - sn = ufe.GlobalSelection.get() - sn.clear() - sn.append(self.b) - - # Hide B - with self.assertRaises(RuntimeError): - cmds.hide() - - # Check that nothing was written to the session layer - self.assertIsNotNone(sessionLayer) - self.assertIsNone(sessionLayer.GetPrimAtPath('/B')) - - # Check that no visibility changes were done in any layer - self.assertFalse(stage.GetAttributeAtPath('/B.visibility').HasAuthoredValue()) if __name__ == '__main__':