Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MAYA-126040 improve preventing edits #2986

Merged
merged 1 commit into from
Mar 30, 2023
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
245 changes: 245 additions & 0 deletions doc/EditRouting.md
Original file line number Diff line number Diff line change
@@ -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()

```
14 changes: 12 additions & 2 deletions lib/mayaUsd/ufe/UsdAttributeHolder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion lib/mayaUsd/ufe/UsdUndoDuplicateCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "private/UfeNotifGuard.h"
#include "private/Utils.h"

#include <mayaUsd/base/tokens.h>
#include <mayaUsd/ufe/Utils.h>
#include <mayaUsd/utils/editRouter.h>
#include <mayaUsd/utils/loadRules.h>
Expand Down Expand Up @@ -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() { }
Expand Down
3 changes: 2 additions & 1 deletion lib/mayaUsd/ufe/UsdUndoInsertChildCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "private/UfeNotifGuard.h"
#include "private/Utils.h"

#include <mayaUsd/base/tokens.h>
#include <mayaUsd/utils/editRouter.h>
#include <mayaUsd/utils/layers.h>
#include <mayaUsd/utils/loadRules.h>
Expand Down Expand Up @@ -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() { }
Expand Down
21 changes: 7 additions & 14 deletions lib/mayaUsd/ufe/UsdUndoVisibleCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
//
#include "UsdUndoVisibleCommand.h"

#include <mayaUsd/base/tokens.h>
#include <mayaUsd/ufe/Utils.h>
#include <mayaUsd/undo/UsdUndoBlock.h>
#include <mayaUsd/utils/editRouter.h>
Expand All @@ -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() { }
Expand All @@ -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<UsdUndoVisibleCommand>(prim, vis, layer);
return std::make_shared<UsdUndoVisibleCommand>(prim, vis);
}

void UsdUndoVisibleCommand::execute()
Expand Down
5 changes: 1 addition & 4 deletions lib/mayaUsd/ufe/UsdUndoVisibleCommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,7 @@ class MAYAUSD_CORE_PUBLIC UsdUndoVisibleCommand : public Ufe::UndoableCommand
typedef std::shared_ptr<UsdUndoVisibleCommand> 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.
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 @@ -25,6 +25,7 @@ if(CMAKE_UFE_V2_FEATURES_AVAILABLE)
testComboCmd.py
testContextOps.py
testDuplicateCmd.py
testEditRouting.py
testGroupCmd.py
testMoveCmd.py
testObject3d.py
Expand Down
Loading