diff --git a/doc/EditRouting.md b/doc/EditRouting.md index 3c8a9aa4a2..ffc495ce94 100644 --- a/doc/EditRouting.md +++ b/doc/EditRouting.md @@ -54,10 +54,11 @@ data indexed by USD tokens (TfToken): 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: +four categories: - Simple commands - Attributes +- Prim metadata - Maya references The following sections describe the input and output of each category. Each @@ -121,6 +122,45 @@ def routeAttrToSessionLayer(context, routingData): routingData['layer'] = prim.GetStage().GetSessionLayer().identifier ``` +### Prim Metadata + +Inputs: +- prim: the USD prim (UsdPrim) that is being affected. +- operation: the operation name (TfToken). Always 'primMetadata'. +- primMetadata: the metadata name (TfToken), e.g. "variantSelection" +- keyPath: the path of the edited key if the metadata is dict-valued (TfToken), + e.g. the variantSet name for "variantSelection" metadata, the key of a "customData". + +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 primMetadata edit router: + +```Python +def routeVariantSelectionToSessionLayer(context, routingData): + ''' + Edit router implementation for 'primMetadata' operations that routes + variant selections within variantSets named 'mySessionVariant' to the + session layer of the stage that contains the prim. + ''' + prim = context.get('prim') + if prim is None: + return + + metadataName = context.get('primMetadata') + if metadataName != "variantSelection": + return + + variantSetName = context.get('keyPath') + if variantSetName != "mySessionVariant": + return + + routingData['layer'] = prim.GetStage().GetSessionLayer().identifier + +mayaUsd.lib.registerEditRouter('primMetadata', routeVariantSelectionToSessionLayer) +``` + ### Maya references The maya reference edit routing is more complex than the other ones. It is @@ -325,6 +365,7 @@ could be used: import mayaUsd.lib sessionAttributes = set(['visibility', 'radius']) +sessionVariantSets = set(['rigVariants', 'proxyVariants']) def routeToSessionLayer(context, routingData): ''' @@ -354,6 +395,27 @@ def routeAttrToSessionLayer(context, routingData): routingData['layer'] = prim.GetStage().GetSessionLayer().identifier +def routeVariantSelectionToSessionLayer(context, routingData): + ''' + Edit router implementation for 'primMetadata' operations that routes + some variantSelection 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 + + metadataName = context.get('primMetadata') + if metadataName != 'variantSelection': + return + + variantSetName = context.get('keyPath') + if variantSetName not in sessionVariantSets: + return + + routingData['layer'] = prim.GetStage().GetSessionLayer().identifier + def registerAttributeEditRouter(): ''' Register an edit router for the 'attribute' operation that routes to @@ -368,8 +430,16 @@ def registerVisibilityEditRouter(): ''' mayaUsd.lib.registerEditRouter('visibility', routeToSessionLayer) +def registerPrimMetadataEditRouter(): + ''' + Register an edit router for the 'primMetadata' operation that routes to + the session layer. + ''' + mayaUsd.lib.registerEditRouter('primMetadata', routeVariantSelectionToSessionLayer) + def registerEditRouters(): registerAttributeEditRouter() registerVisibilityEditRouter() + registerPrimMetadataEditRouter() ``` diff --git a/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py b/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py index 033b99b830..813344a013 100644 --- a/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py +++ b/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py @@ -20,6 +20,7 @@ from .material_custom_control import MaterialCustomControl import collections +import contextlib import fnmatch import re import ufe @@ -136,6 +137,15 @@ def onReplace(self, *args): # Nothing needed here since we don't create any UI. pass +@contextlib.contextmanager +def PrimCustomDataEditRouting(prim, *args): + ''' + A context manager that activates prim customData editRouting. + ''' + # Note: the edit router context must be kept alive in a variable. + ctx = mayaUsdUfe.PrimMetadataEditRouterContext(prim, Sdf.PrimSpec.CustomDataKey, *args) + yield + class MetaDataCustomControl(object): '''Custom control for all prim metadata we want to display.''' def __init__(self, item, prim, useNiceName): @@ -220,11 +230,7 @@ def onReplace(self, *args): # that case we don't need to update our controls since none will change. pass - def refresh(self): - # PrimPath - cmds.textFieldGrp(self.primPath, edit=True, text=str(self.prim.GetPath())) - - # Kind + def _refreshKind(self): model = Usd.ModelAPI(self.prim) primKind = model.GetKind() if not primKind: @@ -233,6 +239,13 @@ def refresh(self): else: cmds.optionMenuGrp(self.kind, edit=True, value=primKind) + def refresh(self): + # PrimPath + cmds.textFieldGrp(self.primPath, edit=True, text=str(self.prim.GetPath())) + + # Kind + self._refreshKind() + # Active cmds.checkBoxGrp(self.active, edit=True, value1=self.prim.IsActive()) @@ -246,8 +259,14 @@ def refresh(self): def _onKindChanged(self, value): with mayaUsdLib.UsdUndoBlock(): - model = Usd.ModelAPI(self.prim) - model.SetKind(value) + try: + usdUfe.SetKindCommand(self.prim, value).execute() + except Exception as ex: + # Note: the command might not work because there is a stronger + # opinion or an editRouting prevention so update the option menu + self._refreshKind() + cmds.error(str(ex)) + def _onActiveChanged(self, value): with mayaUsdLib.UsdUndoBlock(): @@ -704,9 +723,15 @@ def onCreate(self, *args): def onReplace(self, *args): pass - def clear(self): + def _clearUseOutlinerColor(self): cmds.checkBoxGrp(self.useOutlinerColor, edit=True, v1=False) + + def _clearOutlinerColor(self): cmds.colorSliderGrp(self.outlinerColor, edit=True, rgb=(0,0,0)) + + def clear(self): + self._clearUseOutlinerColor() + self._clearOutlinerColor() def refresh(self): try: @@ -715,6 +740,8 @@ def refresh(self): useOutlinerColor = self.item.getGroupMetadata(self.GROUP, self.USE_OUTLINER_COLOR) if not useOutlinerColor.empty() and (useOutlinerColor.typeName() == 'bool'): cmds.checkBoxGrp(self.useOutlinerColor, edit=True, v1=bool(useOutlinerColor)) + else: + self._clearUseOutlinerColor() outlinerColor = self.item.getGroupMetadata(self.GROUP, self.OUTLINER_COLOR) if not outlinerColor.empty() and (outlinerColor.typeName() == "ufe.Vector3d"): @@ -722,29 +749,35 @@ def refresh(self): clr = ufe.Vector3d(outlinerColor) cmds.colorSliderGrp(self.outlinerColor, edit=True, rgb=(clr.x(), clr.y(), clr.z())) + else: + self._clearOutlinerColor() else: # Get the custom data directly from USD. useOutlinerColor = self.prim.GetCustomDataByKey(self.USE_OUTLINER_COLOR) if useOutlinerColor is not None and isinstance(useOutlinerColor, bool): cmds.checkBoxGrp(self.useOutlinerColor, edit=True, v1=useOutlinerColor) + else: + self._clearUseOutlinerColor() outlinerColor = self.prim.GetCustomDataByKey(self.OUTLINER_COLOR) if outlinerColor is not None and isinstance(outlinerColor, Gf.Vec3d): # Color is stored as double3 USD custom data. cmds.colorSliderGrp(self.outlinerColor, edit=True, rgb=(outlinerColor[0], outlinerColor[1], outlinerColor[2])) + else: + self._clearOutlinerColor() except: self.clear() def _updateTextColorChanged(self): '''Update the text color custom data for this prim based on the values set in the two fields.''' - currEditTarget = None + # Get the value of "Use Outliner Color" checkbox. + useTextColor = cmds.checkBoxGrp(self.useOutlinerColor, query=True, v1=True) + # Get the value of "Outliner Color" color slider. + rgb = cmds.colorSliderGrp(self.outlinerColor, query=True, rgbValue=True) try: if self.useMetadata: - useTextColor = cmds.checkBoxGrp(self.useOutlinerColor, query=True, v1=True) - rgb = cmds.colorSliderGrp(self.outlinerColor, query=True, rgbValue=True) - # Get ufe commands for the two metadata. cmd1 = self.item.setGroupMetadataCmd(self.GROUP, self.USE_OUTLINER_COLOR, useTextColor) ufeVec = ufe.Vector3d(rgb[0], rgb[1], rgb[2]) @@ -756,20 +789,17 @@ def _updateTextColorChanged(self): else: with mayaUsdLib.UsdUndoBlock(): # As initially decided write out the color custom data to the session layer. - stage = self.prim.GetStage() - currEditTarget = stage.GetEditTarget() - stage.SetEditTarget(stage.GetSessionLayer()) - - # Get the value of "Use Outliner Color" checkbox and set in custom data. - useTextColor = cmds.checkBoxGrp(self.useOutlinerColor, query=True, v1=True) - self.prim.SetCustomDataByKey(self.USE_OUTLINER_COLOR, useTextColor) - - # Get the value of "Outliner Color" color slider and set in custom data. - rgb = cmds.colorSliderGrp(self.outlinerColor, query=True, rgbValue=True) - self.prim.SetCustomDataByKey(self.OUTLINER_COLOR, Gf.Vec3d(rgb[0], rgb[1], rgb[2])) - finally: - if currEditTarget is not None: - stage.SetEditTarget(currEditTarget) + # It still can be edit-routed as a 'primMetadata' operation. + fallbackLayer = self.prim.GetStage().GetSessionLayer() + with PrimCustomDataEditRouting(self.prim, self.USE_OUTLINER_COLOR, fallbackLayer): + self.prim.SetCustomDataByKey(self.USE_OUTLINER_COLOR, useTextColor) + with PrimCustomDataEditRouting(self.prim, self.OUTLINER_COLOR, fallbackLayer): + self.prim.SetCustomDataByKey(self.OUTLINER_COLOR, Gf.Vec3d(rgb[0], rgb[1], rgb[2])) + except Exception as ex: + # Note: the command might not work because there is a stronger + # opinion or an editRouting prevention so update the metadata controls. + self.refresh() + cmds.error(str(ex)) def _onUseOutlinerColorChanged(self, value): self._updateTextColorChanged() diff --git a/lib/usdUfe/base/tokens.h b/lib/usdUfe/base/tokens.h index 24d0ddc01a..706efd3ad5 100644 --- a/lib/usdUfe/base/tokens.h +++ b/lib/usdUfe/base/tokens.h @@ -39,6 +39,8 @@ namespace USDUFE_NS_DEF { /* Stage received in the context of some router */ \ ((Stage, "stage")) \ ((EditTarget, "editTarget")) \ + /* Metadata key path received in the context */ \ + ((KeyPath, "keyPath")) \ \ /* Routing operations */ \ \ @@ -46,6 +48,7 @@ namespace USDUFE_NS_DEF { ((RouteDuplicate, "duplicate")) \ ((RouteVisibility, "visibility")) \ ((RouteAttribute, "attribute")) \ + ((RoutePrimMetadata, "primMetadata")) \ ((RouteDelete, "delete")) \ ((RouteTransform, "transform")) \ \ diff --git a/lib/usdUfe/python/wrapCommands.cpp b/lib/usdUfe/python/wrapCommands.cpp index 9182a547dd..4c1c4a7fd8 100644 --- a/lib/usdUfe/python/wrapCommands.cpp +++ b/lib/usdUfe/python/wrapCommands.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -67,6 +68,12 @@ UsdUfe::UsdUndoToggleInstanceableCommand* ToggleInstanceableCommandInit(const PX return new UsdUfe::UsdUndoToggleInstanceableCommand(prim); } +UsdUfe::UsdUndoSetKindCommand* +SetKindCommandInit(const PXR_NS::UsdPrim& prim, const PXR_NS::TfToken& kind) +{ + return new UsdUfe::UsdUndoSetKindCommand(prim, kind); +} + UsdUfe::UsdUndoLoadPayloadCommand* LoadPayloadCommandInit(const PXR_NS::UsdPrim& prim, PXR_NS::UsdLoadPolicy policy) { @@ -165,6 +172,14 @@ void wrapCommands() .def("undo", &UsdUfe::UsdUndoToggleInstanceableCommand::undo) .def("redo", &UsdUfe::UsdUndoToggleInstanceableCommand::redo); } + { + using This = UsdUfe::UsdUndoSetKindCommand; + class_("SetKindCommand", no_init) + .def("__init__", make_constructor(SetKindCommandInit)) + .def("execute", &UsdUfe::UsdUndoSetKindCommand::execute) + .def("undo", &UsdUfe::UsdUndoSetKindCommand::undo) + .def("redo", &UsdUfe::UsdUndoSetKindCommand::redo); + } { using This = UsdUfe::UsdUndoLoadPayloadCommand; class_("LoadPayloadCommand", no_init) diff --git a/lib/usdUfe/python/wrapEditRouter.cpp b/lib/usdUfe/python/wrapEditRouter.cpp index 5581b46c9c..acc50c4a24 100644 --- a/lib/usdUfe/python/wrapEditRouter.cpp +++ b/lib/usdUfe/python/wrapEditRouter.cpp @@ -198,4 +198,14 @@ void wrapEditRouter() using AttrThis = UsdUfe::AttributeEditRouterContext; class_("AttributeEditRouterContext", no_init) .def("__init__", make_constructor(AttributeEditRouterContextInit)); + + using PrimMdThis = UsdUfe::PrimMetadataEditRouterContext; + class_("PrimMetadataEditRouterContext", no_init) + .def(init()) + .def(init()) + .def(init< + const PXR_NS::UsdPrim&, + const PXR_NS::TfToken&, + const PXR_NS::TfToken&, + const PXR_NS::SdfLayerHandle&>()); } diff --git a/lib/usdUfe/ufe/SetVariantSelectionCommand.cpp b/lib/usdUfe/ufe/SetVariantSelectionCommand.cpp index d8df2833d6..9bc15f03dc 100644 --- a/lib/usdUfe/ufe/SetVariantSelectionCommand.cpp +++ b/lib/usdUfe/ufe/SetVariantSelectionCommand.cpp @@ -16,6 +16,7 @@ #include "SetVariantSelectionCommand.h" #include +#include #include @@ -50,15 +51,20 @@ SetVariantSelectionCommand::SetVariantSelectionCommand( void SetVariantSelectionCommand::redo() { + const PXR_NS::TfToken metadataKeyPath(_varSet.GetName()); + + PrimMetadataEditRouterContext ctx( + _prim, PXR_NS::SdfFieldKeys->VariantSelection, metadataKeyPath); + std::string errMsg; if (!UsdUfe::isPrimMetadataEditAllowed( - _prim, - PXR_NS::SdfFieldKeys->VariantSelection, - PXR_NS::TfToken(_varSet.GetName()), - &errMsg)) { + _prim, PXR_NS::SdfFieldKeys->VariantSelection, metadataKeyPath, &errMsg)) { throw std::runtime_error(errMsg.c_str()); } + // Backup the destination layer for consistent undo. + _dstLayer = _prim.GetStage()->GetEditTarget().GetLayer(); + // Make a copy of the global selection, to restore it on undo. auto globalSn = Ufe::GlobalSelection::get(); _savedSn.replaceWith(*globalSn); @@ -69,6 +75,8 @@ void SetVariantSelectionCommand::redo() void SetVariantSelectionCommand::undo() { + PrimMetadataEditRouterContext ctx(_prim.GetStage(), _dstLayer); + std::string errMsg; if (!UsdUfe::isPrimMetadataEditAllowed( _prim, diff --git a/lib/usdUfe/ufe/SetVariantSelectionCommand.h b/lib/usdUfe/ufe/SetVariantSelectionCommand.h index cb7e3865bf..51f509a33f 100644 --- a/lib/usdUfe/ufe/SetVariantSelectionCommand.h +++ b/lib/usdUfe/ufe/SetVariantSelectionCommand.h @@ -54,12 +54,13 @@ class USDUFE_PUBLIC SetVariantSelectionCommand : public Ufe::UndoableCommand void undo() override; private: - const Ufe::Path _path; - PXR_NS::UsdPrim _prim; - PXR_NS::UsdVariantSet _varSet; - const std::string _oldSelection; - const std::string _newSelection; - Ufe::Selection _savedSn; // For global selection save and restore. + const Ufe::Path _path; + PXR_NS::UsdPrim _prim; + PXR_NS::UsdVariantSet _varSet; + const std::string _oldSelection; + const std::string _newSelection; + Ufe::Selection _savedSn; // For global selection save and restore. + PXR_NS::SdfLayerHandle _dstLayer; // To ensure consistent editTarget at undo. }; } // namespace USDUFE_NS_DEF diff --git a/lib/usdUfe/ufe/UsdUndoAddRefOrPayloadCommand.cpp b/lib/usdUfe/ufe/UsdUndoAddRefOrPayloadCommand.cpp index f0e5461cca..6713869c21 100644 --- a/lib/usdUfe/ufe/UsdUndoAddRefOrPayloadCommand.cpp +++ b/lib/usdUfe/ufe/UsdUndoAddRefOrPayloadCommand.cpp @@ -15,6 +15,8 @@ // #include "UsdUndoAddRefOrPayloadCommand.h" +#include + #include #include #include @@ -94,10 +96,16 @@ void UsdUndoAddRefOrPayloadCommand::executeImplementation() if (_isPayload) { SdfPayload payload(_filePath, primPath); UsdPayloads primPayloads = _prim.GetPayloads(); + + PrimMetadataEditRouterContext ctx(_prim, SdfFieldKeys->Payload); + primPayloads.AddPayload(payload, _listPos); } else { SdfReference ref(_filePath, primPath); UsdReferences primRefs = _prim.GetReferences(); + + PrimMetadataEditRouterContext ctx(_prim, SdfFieldKeys->References); + primRefs.AddReference(ref, _listPos); } } diff --git a/lib/usdUfe/ufe/UsdUndoClearPayloadsCommand.cpp b/lib/usdUfe/ufe/UsdUndoClearPayloadsCommand.cpp index dad762d246..886fb9e564 100644 --- a/lib/usdUfe/ufe/UsdUndoClearPayloadsCommand.cpp +++ b/lib/usdUfe/ufe/UsdUndoClearPayloadsCommand.cpp @@ -16,6 +16,8 @@ #include "UsdUndoClearPayloadsCommand.h" +#include + #include namespace USDUFE_NS_DEF { @@ -32,6 +34,8 @@ void UsdUndoClearPayloadsCommand::executeImplementation() if (!_prim.IsValid()) return; + PrimMetadataEditRouterContext ctx(_prim, SdfFieldKeys->Payload); + _prim.GetPayloads().ClearPayloads(); } diff --git a/lib/usdUfe/ufe/UsdUndoClearReferencesCommand.cpp b/lib/usdUfe/ufe/UsdUndoClearReferencesCommand.cpp index 9cd7d20808..8e27f30439 100644 --- a/lib/usdUfe/ufe/UsdUndoClearReferencesCommand.cpp +++ b/lib/usdUfe/ufe/UsdUndoClearReferencesCommand.cpp @@ -16,6 +16,8 @@ #include "UsdUndoClearReferencesCommand.h" +#include + #include namespace USDUFE_NS_DEF { @@ -32,6 +34,8 @@ void UsdUndoClearReferencesCommand::executeImplementation() if (!_prim.IsValid()) return; + PrimMetadataEditRouterContext ctx(_prim, SdfFieldKeys->References); + _prim.GetReferences().ClearReferences(); } diff --git a/lib/usdUfe/ufe/UsdUndoClearSceneItemMetadataCommand.cpp b/lib/usdUfe/ufe/UsdUndoClearSceneItemMetadataCommand.cpp index 8ea8abdb3a..415c872416 100644 --- a/lib/usdUfe/ufe/UsdUndoClearSceneItemMetadataCommand.cpp +++ b/lib/usdUfe/ufe/UsdUndoClearSceneItemMetadataCommand.cpp @@ -18,6 +18,8 @@ #include #include +#include +#include #include #include @@ -52,17 +54,26 @@ void ClearSceneItemMetadataCommand::executeImplementation() const PXR_NS::UsdPrim prim = _stage->GetPrimAtPath(_primPath); if (_group.empty()) { // If this is not a grouped meta data, remove the key - prim.ClearCustomDataByKey(TfToken(_key)); + PXR_NS::TfToken key(_key); + PrimMetadataEditRouterContext ctx(prim, SdfFieldKeys->CustomData, key); + prim.ClearCustomDataByKey(key); } else { // When the group name starts with "SessionLayer-", remove that prefix - // and clear in the session layer. + // and clear in the session layer if the operation is not editRouted. std::string prefixlessGroupName; if (isSessionLayerGroupMetadata(_group, &prefixlessGroupName)) { - PXR_NS::UsdEditContext editCtx(_stage, _stage->GetSessionLayer()); - PXR_NS::TfToken fullKey(prefixlessGroupName + std::string(":") + _key); + PXR_NS::TfToken fullKey(prefixlessGroupName + std::string(":") + _key); + + PrimMetadataEditRouterContext ctx( + prim, + PXR_NS::SdfFieldKeys->CustomData, + fullKey, + /*fallbackLayer=*/_stage->GetSessionLayer()); + prim.ClearCustomDataByKey(fullKey); } else { - PXR_NS::TfToken fullKey(_group + std::string(":") + _key); + PXR_NS::TfToken fullKey(_group + std::string(":") + _key); + PrimMetadataEditRouterContext ctx(prim, SdfFieldKeys->CustomData, fullKey); prim.ClearCustomDataByKey(fullKey); } } diff --git a/lib/usdUfe/ufe/UsdUndoSetKindCommand.cpp b/lib/usdUfe/ufe/UsdUndoSetKindCommand.cpp index 6a78b19dea..db6bbc6a60 100644 --- a/lib/usdUfe/ufe/UsdUndoSetKindCommand.cpp +++ b/lib/usdUfe/ufe/UsdUndoSetKindCommand.cpp @@ -15,7 +15,9 @@ // #include "UsdUndoSetKindCommand.h" +#include #include +#include #include #include @@ -50,8 +52,17 @@ UsdUndoSetKindCommand::create(const PXR_NS::UsdPrim& prim, const PXR_NS::TfToken void UsdUndoSetKindCommand::execute() { - UsdUndoBlock undoBlock(&_undoableItem); + PrimMetadataEditRouterContext ctx(_prim, PXR_NS::SdfFieldKeys->Kind); + + std::string errMsg; + if (!UsdUfe::isPrimMetadataEditAllowed( + _prim, PXR_NS::SdfFieldKeys->Kind, PXR_NS::TfToken(), &errMsg)) { + // Note: we don't throw an exception because this would break bulk actions. + TF_RUNTIME_ERROR(errMsg); + return; + } + UsdUndoBlock undoBlock(&_undoableItem); PXR_NS::UsdModelAPI(_prim).SetKind(_kind); } diff --git a/lib/usdUfe/ufe/UsdUndoSetSceneItemMetadataCommand.cpp b/lib/usdUfe/ufe/UsdUndoSetSceneItemMetadataCommand.cpp index bfd588ccb1..80788473de 100644 --- a/lib/usdUfe/ufe/UsdUndoSetSceneItemMetadataCommand.cpp +++ b/lib/usdUfe/ufe/UsdUndoSetSceneItemMetadataCommand.cpp @@ -18,6 +18,8 @@ #include #include +#include +#include #include #include @@ -31,6 +33,24 @@ namespace USDUFE_NS_DEF { +namespace { + +void setSceneItemCustomDataByKey( + const PXR_NS::UsdPrim& prim, + const PXR_NS::TfToken& key, + const Ufe::Value& value) +{ + std::string errMsg; + if (!UsdUfe::isPrimMetadataEditAllowed(prim, PXR_NS::SdfFieldKeys->CustomData, key, &errMsg)) { + // Note: we don't throw an exception because this would break bulk actions. + TF_RUNTIME_ERROR(errMsg); + } else { + prim.SetCustomDataByKey(key, ufeValueToVtValue(value)); + } +} + +} // namespace + USDUFE_VERIFY_CLASS_SETUP( UsdUfe::UsdUndoableCommand, SetSceneItemMetadataCommand); @@ -63,9 +83,11 @@ SetSceneItemMetadataCommand::SetSceneItemMetadataCommand( void SetSceneItemMetadataCommand::setKeyMetadata() { const PXR_NS::UsdPrim prim = _stage->GetPrimAtPath(_primPath); + const PXR_NS::TfToken key(_key); // If this is not a grouped metadata, set the _value directly on the _key - prim.SetCustomDataByKey(TfToken(_key), ufeValueToVtValue(_value)); + PrimMetadataEditRouterContext ctx(prim, PXR_NS::SdfFieldKeys->CustomData, key); + setSceneItemCustomDataByKey(prim, key, _value); } void SetSceneItemMetadataCommand::setGroupMetadata() @@ -73,15 +95,22 @@ void SetSceneItemMetadataCommand::setGroupMetadata() const PXR_NS::UsdPrim prim = _stage->GetPrimAtPath(_primPath); // When the group name starts with "SessionLayer-", remove that prefix - // and write in the session layer. + // and write in the session layer if the operation is not editRouted. std::string prefixlessGroupName; if (isSessionLayerGroupMetadata(_group, &prefixlessGroupName)) { - PXR_NS::UsdEditContext editCtx(_stage, _stage->GetSessionLayer()); - PXR_NS::TfToken fullKey(prefixlessGroupName + std::string(":") + _key); - prim.SetCustomDataByKey(fullKey, ufeValueToVtValue(_value)); + PXR_NS::TfToken fullKey(prefixlessGroupName + std::string(":") + _key); + + PrimMetadataEditRouterContext ctx( + prim, + PXR_NS::SdfFieldKeys->CustomData, + fullKey, + /*fallbackLayer=*/_stage->GetSessionLayer()); + + setSceneItemCustomDataByKey(prim, fullKey, _value); } else { - PXR_NS::TfToken fullKey(_group + std::string(":") + _key); - prim.SetCustomDataByKey(fullKey, ufeValueToVtValue(_value)); + PXR_NS::TfToken fullKey(_group + std::string(":") + _key); + PrimMetadataEditRouterContext ctx(prim, PXR_NS::SdfFieldKeys->CustomData, fullKey); + setSceneItemCustomDataByKey(prim, fullKey, _value); } } diff --git a/lib/usdUfe/ufe/UsdUndoToggleActiveCommand.cpp b/lib/usdUfe/ufe/UsdUndoToggleActiveCommand.cpp index 27233dbadf..3ab644cbf7 100644 --- a/lib/usdUfe/ufe/UsdUndoToggleActiveCommand.cpp +++ b/lib/usdUfe/ufe/UsdUndoToggleActiveCommand.cpp @@ -18,6 +18,7 @@ #include #include +#include namespace USDUFE_NS_DEF { @@ -38,6 +39,8 @@ void UsdUndoToggleActiveCommand::executeImplementation() if (!prim.IsValid()) return; + PrimMetadataEditRouterContext ctx(prim, PXR_NS::SdfFieldKeys->Active); + std::string errMsg; if (!UsdUfe::isPrimMetadataEditAllowed( prim, PXR_NS::SdfFieldKeys->Active, PXR_NS::TfToken(), &errMsg)) { diff --git a/lib/usdUfe/ufe/UsdUndoToggleInstanceableCommand.cpp b/lib/usdUfe/ufe/UsdUndoToggleInstanceableCommand.cpp index e755bc0833..c6f3a1ec2a 100644 --- a/lib/usdUfe/ufe/UsdUndoToggleInstanceableCommand.cpp +++ b/lib/usdUfe/ufe/UsdUndoToggleInstanceableCommand.cpp @@ -17,6 +17,7 @@ #include "UsdUndoToggleInstanceableCommand.h" #include +#include namespace USDUFE_NS_DEF { @@ -39,6 +40,8 @@ void UsdUndoToggleInstanceableCommand::executeImplementation() if (!prim.IsValid()) return; + PrimMetadataEditRouterContext ctx(prim, PXR_NS::SdfFieldKeys->Instanceable); + std::string errMsg; if (!UsdUfe::isPrimMetadataEditAllowed( prim, PXR_NS::SdfFieldKeys->Instanceable, PXR_NS::TfToken(), &errMsg)) { diff --git a/lib/usdUfe/utils/editRouter.cpp b/lib/usdUfe/utils/editRouter.cpp index 90d364e70f..83a3b969d1 100644 --- a/lib/usdUfe/utils/editRouter.cpp +++ b/lib/usdUfe/utils/editRouter.cpp @@ -275,6 +275,38 @@ getAttrEditRouterLayer(const PXR_NS::UsdPrim& prim, const PXR_NS::TfToken& attrN return _extractLayer(found->second); } +PXR_NS::SdfLayerHandle getPrimMetadataEditRouterLayer( + const PXR_NS::UsdPrim& prim, + const PXR_NS::TfToken& metadataName, + const PXR_NS::TfToken& metadataKeyPath) +{ + static const PXR_NS::TfToken metadataOp(EditRoutingTokens->RoutePrimMetadata); + + const EditRouter::Ptr dstEditRouter = getEditRouter(metadataOp); + if (!dstEditRouter) + return nullptr; + + // Optimize the case where we have a per-stage layer routing. + // This avoid creating dictionaries just to pass and receive a value. + if (auto layerRouter = std::dynamic_pointer_cast(dstEditRouter)) + return layerRouter->getLayerForStage(prim.GetStage()); + + PXR_NS::VtDictionary context; + PXR_NS::VtDictionary routingData; + context[EditRoutingTokens->Prim] = PXR_NS::VtValue(prim); + context[EditRoutingTokens->Operation] = metadataOp; + context[EditRoutingTokens->KeyPath] = PXR_NS::VtValue(metadataKeyPath); + context[metadataOp] = PXR_NS::VtValue(metadataName); + (*dstEditRouter)(context, routingData); + + // Try to retrieve the layer from the routing data. + const auto found = routingData.find(EditRoutingTokens->Layer); + if (found == routingData.end()) + return nullptr; + + return _extractLayer(found->second); +} + PXR_NS::UsdEditTarget getEditRouterEditTarget(const PXR_NS::TfToken& operation, const PXR_NS::UsdPrim& prim) { diff --git a/lib/usdUfe/utils/editRouter.h b/lib/usdUfe/utils/editRouter.h index de376741e6..6c6cb18216 100644 --- a/lib/usdUfe/utils/editRouter.h +++ b/lib/usdUfe/utils/editRouter.h @@ -111,6 +111,14 @@ USDUFE_PUBLIC PXR_NS::SdfLayerHandle getAttrEditRouterLayer(const PXR_NS::UsdPrim& prim, const PXR_NS::TfToken& attrName); +// Retrieve the layer for the prim metadata operation. If no edit router for the +// "primMetadata" operation is found, a nullptr is returned. +USDUFE_PUBLIC +PXR_NS::SdfLayerHandle getPrimMetadataEditRouterLayer( + const PXR_NS::UsdPrim& prim, + const PXR_NS::TfToken& metadataName, + const PXR_NS::TfToken& metadataKeyPath); + // Utility function that returns a UsdEditTarget for the argument operation. // If no edit router exists for that operation, a null UsdEditTarget is returned. // The edit router is given the prim in the context with key "prim", and is diff --git a/lib/usdUfe/utils/editRouterContext.cpp b/lib/usdUfe/utils/editRouterContext.cpp index 5003309d18..d8bbf1dde2 100644 --- a/lib/usdUfe/utils/editRouterContext.cpp +++ b/lib/usdUfe/utils/editRouterContext.cpp @@ -131,4 +131,35 @@ AttributeEditRouterContext::AttributeEditRouterContext( { } +PXR_NS::SdfLayerHandle PrimMetadataEditRouterContext::getPrimMetadataLayer( + const PXR_NS::UsdPrim& prim, + const PXR_NS::TfToken& metadataName, + const PXR_NS::TfToken& metadataKeyPath, + const PXR_NS::SdfLayerHandle& fallbackLayer) +{ + if (isTargetAlreadySet()) + return nullptr; + + auto routerLayer = getPrimMetadataEditRouterLayer(prim, metadataName, metadataKeyPath); + + return routerLayer ? routerLayer : fallbackLayer; +} + +PrimMetadataEditRouterContext::PrimMetadataEditRouterContext( + const PXR_NS::UsdPrim& prim, + const PXR_NS::TfToken& metadataName, + const PXR_NS::TfToken& metadataKeyPath, + const PXR_NS::SdfLayerHandle& fallbackLayer) + : StackedEditRouterContext( + prim.GetStage(), + getPrimMetadataLayer(prim, metadataName, metadataKeyPath, fallbackLayer)) +{ +} + +PrimMetadataEditRouterContext::PrimMetadataEditRouterContext( + const PXR_NS::UsdStagePtr& stage, + const PXR_NS::SdfLayerHandle& layer) + : StackedEditRouterContext(stage, layer) +{ +} } // namespace USDUFE_NS_DEF diff --git a/lib/usdUfe/utils/editRouterContext.h b/lib/usdUfe/utils/editRouterContext.h index 24ffa8569b..e7953dc111 100644 --- a/lib/usdUfe/utils/editRouterContext.h +++ b/lib/usdUfe/utils/editRouterContext.h @@ -126,6 +126,46 @@ class USDUFE_PUBLIC AttributeEditRouterContext : public StackedEditRouterContext getAttributeLayer(const PXR_NS::UsdPrim& prim, const PXR_NS::TfToken& attributeName); }; +/*! \brief Select the target layer when modifying USD prim metadata, via the edit router. + * + * Commands and other code that wish to be routable when modifying an USD prim metadata + * should use this instead of the native USD USdEditContext class. + * + * Support nesting properly, so that if a composite command is routed to a layer, + * all sub-commands will use that layer and not individually routed layers. The + * nesting is per-thread. + * + * We may add ways for edit routers of sub-command to force routing to a different + * layer in the future. Using this class will make this transparent. + */ +class USDUFE_PUBLIC PrimMetadataEditRouterContext : public StackedEditRouterContext +{ +public: + /*! \brief Route a metadata operation on a prim for the given metadata / metadataKeyPath. + * If there is no editRouting for this metadata and a non-null fallbackLayer is given, + * it will be used as edit target. + */ + PrimMetadataEditRouterContext( + const PXR_NS::UsdPrim& prim, + const pxr::TfToken& metadataName, + const pxr::TfToken& metadataKeyPath = pxr::TfToken {}, + const PXR_NS::SdfLayerHandle& fallbackLayer = PXR_NS::SdfLayerHandle {}); + + /*! \brief Route to the given stage and layer. + * Should be used in undo to ensure the same target is used as in the initial execution. + */ + PrimMetadataEditRouterContext( + const PXR_NS::UsdStagePtr& stage, + const PXR_NS::SdfLayerHandle& layer); + +private: + PXR_NS::SdfLayerHandle getPrimMetadataLayer( + const PXR_NS::UsdPrim& prim, + const PXR_NS::TfToken& metadataName, + const PXR_NS::TfToken& metadataKeyPath, + const PXR_NS::SdfLayerHandle& fallbackLayer); +}; + } // namespace USDUFE_NS_DEF #endif // USDUFE_EDITROUTERCONTEXT_H diff --git a/test/lib/ufe/CMakeLists.txt b/test/lib/ufe/CMakeLists.txt index bcbf7c94fd..f0bbec7f7c 100644 --- a/test/lib/ufe/CMakeLists.txt +++ b/test/lib/ufe/CMakeLists.txt @@ -12,6 +12,7 @@ set(TEST_SCRIPT_FILES testDuplicateCmd.py testDuplicateProxyShape.py testEditRouting.py + testPrimMetadataEditRouting.py testGroupCmd.py testMatrices.py testMayaPickwalk.py @@ -169,6 +170,7 @@ foreach(script ${TEST_SCRIPT_FILES}) "UFE_CAMERA_HAS_RENDERABLE=${UFE_CAMERA_HAS_RENDERABLE}" "UFE_SCENE_SEGMENT_HANDLER_ROOT_PATH=${UFE_SCENE_SEGMENT_HANDLER_ROOT_PATH}" "UFE_VOLUME_LIGHTS_SUPPORT=${UFE_VOLUME_LIGHTS_SUPPORT}" + "UFE_SCENEITEM_HAS_METADATA=${UFE_SCENEITEM_HAS_METADATA}" ) # Add a ctest label to these tests for easy filtering. diff --git a/test/lib/ufe/testPrimMetadataEditRouting.py b/test/lib/ufe/testPrimMetadataEditRouting.py new file mode 100644 index 0000000000..0dd97c250c --- /dev/null +++ b/test/lib/ufe/testPrimMetadataEditRouting.py @@ -0,0 +1,488 @@ +#!/usr/bin/env python + +import os +import unittest + +from maya import cmds, standalone +from pxr import Sdf, Usd +import ufe + +import mayaUsd.lib +import mayaUsd.ufe +import usdUfe + +import fixturesUtils +import mayaUsd_createStageWithNewLayer +import mayaUtils +import testUtils +import usdUtils + + +##################################################################### +# +# Custom undoable command with metadata editRouting support + + +class CustomSetPrimMetadataCommand(ufe.UndoableCommand): + ''' + Custom UFE undoable command to set an USD prim metadata value + with editRouting support. + ''' + + def __init__(self, sceneItem, name, value): + super(CustomSetPrimMetadataCommand, self).__init__() + self.prim = usdUtils.getPrimFromSceneItem(sceneItem) + self.name = name + self.value = value + + def execute(self): + self.undoItem = mayaUsd.lib.UsdUndoableItem() + with mayaUsd.lib.UsdUndoBlock(self.undoItem): + # Note: the edit router context must be kept alive in a variable. + ctx = mayaUsd.ufe.PrimMetadataEditRouterContext(self.prim, self.name) + self.prim.SetMetadata(self.name, self.value) + + def undo(self): + self.undoItem.undo() + + def redo(self): + self.undoItem.redo() + + +##################################################################### +# +# Maya usd scene used in test + + +def createSimpleSceneItem(): + ''' + Creates the maya scene used by following tests. + ''' + psPathStr = mayaUsd_createStageWithNewLayer.createStageWithNewLayer() + + # Define a single prim. + stage = mayaUsd.lib.GetPrim(psPathStr).GetStage() + prim = stage.DefinePrim('/Xf', 'Xform') + + # Add a subLayer that will be used as editRouting destination by all tests. + # Note: We do not use session layer as some tests verify that we route + # edits that are authored in session by default. + layer = Sdf.Layer.CreateAnonymous("metadataLayerTag") + stage.GetRootLayer().subLayerPaths.append(layer.identifier) + + return ufe.Hierarchy.createItem(ufe.Path([ + ufe.PathString.path(psPathStr).segments[0], + usdUtils.createUfePathSegment(str(prim.GetPath())), + ])) + + +def getMetadataDestinationLayer(prim): + ''' + Returns the editRouting destination layer used by the following tests. + ''' + # Note: We do not use session layer here as some tests verify that we route + # edits that are authored in session by default. + for layer in prim.GetStage().GetLayerStack(False): + if layer.anonymous and layer.identifier.endswith('metadataLayerTag'): + destLayer = layer + break + else: + raise Exception("Could not find expected anonymous layer in layer stack") + return destLayer + + +def varSelectionCmd(sceneItem, variantSetName, variantName): + ''' + Creates an ufe command to perform the given variant selection. + ''' + ctxOps = ufe.ContextOps.contextOps(sceneItem) + return ctxOps.doOpCmd(['Variant Sets', variantSetName, variantName]) + + +def primFromSceneItem(sceneItem): + return usdUtils.getPrimFromSceneItem(sceneItem) + + +##################################################################### +# +# Edit routers used in test + + +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) + + +def routePrimMetadataToDestLayer(context, routingData): + ''' + Edit router for commands, routing primMetadata to the test destination layer. + ''' + prim = context.get('prim') + if prim is None: + print('Prim not in context') + return + + if context.get('operation') == 'primMetadata': + routingData['layer'] = getMetadataDestinationLayer(prim).identifier + + +def routeOnlySessionVariantSelectionToDestLayer(context, rootingData): + ''' + Edit router for commands, routing to the the test destination layer + only variant selections on a variantSet named SessionVariant. + ''' + if context.get('primMetadata') == 'variantSelection': + if context.get('keyPath') == 'SessionVariant': + routePrimMetadataToDestLayer(context, rootingData) + + +##################################################################### +# +# Tests + + +class PrimMetadataEditRoutingTestCase(unittest.TestCase): + '''Verify the Maya Edit Router for prim metadata operations.''' + + pluginsLoaded = False + ufeMetadataSupported = os.getenv('UFE_SCENEITEM_HAS_METADATA', 'FALSE') != '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) + + def tearDown(self): + # Restore default edit routers. + mayaUsd.lib.clearAllEditRouters() + mayaUsd.lib.restoreAllDefaultEditRouters() + + def _verifyEditRoutingForMetadata( + self, + prepCmdFn, + verifyFn, + routeFn=None, + useFastRouting=None, + ): + ''' + Utility to verify edit routing for an ufe undoable command which is setting + or clearing an USD prim metadata value. + It can route using a custom router, or fast routing to the session layer, + else it will route to the session layer using a router command. + ''' + # Create a stage with a single prim. + sceneItem = createSimpleSceneItem() + prim = primFromSceneItem(sceneItem) + stage = prim.GetStage() + dstLayer = getMetadataDestinationLayer(prim) + + # Verify our test case is valid. + self.assertNotEqual(stage.GetEditTarget().GetLayer(), dstLayer) + self.assertTrue(dstLayer.empty) + + # Customize destination prim and get the undoable command to verify. + undoableCmd = prepCmdFn(sceneItem) + + # Setup Edit routing. + if useFastRouting: + if routeFn is not None: + raise Exception('routeToStageLayer and routeFn cannot be used together') + mayaUsd.lib.registerStageLayerEditRouter('primMetadata', stage, dstLayer) + else: + mayaUsd.lib.registerEditRouter('primMetadata', routeFn or routePrimMetadataToDestLayer) + + def verify(): + verifyFn(dstLayer.GetPrimAtPath(prim.GetPath())) + + # Verify that the verify func is valid. + hadPrimSpecBeforeExec = bool(dstLayer.GetPrimAtPath(prim.GetPath())) + if hadPrimSpecBeforeExec: + with self.assertRaises(AssertionError): + verify() + + # Verify that the command is routed to the session layer. + undoableCmd.execute() + verify() + + # Verify that undo is performed in session layer. + undoableCmd.undo() + + hasPrimSpecAfterUndo = bool(dstLayer.GetPrimAtPath(prim.GetPath())) + if hasPrimSpecAfterUndo != hadPrimSpecBeforeExec: + with self.assertRaises(AssertionError): + verify() + + # Verify that redo correctly re-route edits. + undoableCmd.redo() + verify() + + def _verifyEditRoutingForSetMetadata(self, cmdFn, verifyFn, **kwargs): + ''' + Utility to verify edit routing for a command setting a prim metadata value. + ''' + + def verifySpecFn(primSpec): + # verify that we have a valid spec authored. + self.assertIsNotNone(primSpec) + verifyFn(primSpec) + + self._verifyEditRoutingForMetadata(cmdFn, verifySpecFn, **kwargs) + + def _verifyEditRoutingForClearMetadata(self, prepFn, cmdFn, verifyFn, **kwargs): + ''' + Utility to verify edit routing for a command clearing a prim metadata value. + ''' + + def prepCmdFn(sceneItem): + # prepare the prim, adding metadata to be cleared. + prim = primFromSceneItem(sceneItem) + with Usd.EditContext(prim.GetStage(), getMetadataDestinationLayer(prim)): + prepFn(prim) + # return the clear command. + return cmdFn(sceneItem) + + self._verifyEditRoutingForMetadata(prepCmdFn, verifyFn, **kwargs) + + def testPreventEditRoutingOfMetadataOperations(self): + ''' + Test that edit routing can prevent commands for the primMetadata operation. + ''' + # Create a stage with a single prim + sceneItem = createSimpleSceneItem() + prim = primFromSceneItem(sceneItem) + stage = prim.GetStage() + varSet = prim.GetVariantSets().AddVariantSet('TestVariant') + refFile = testUtils.getTestScene('twoSpheres', 'sphere.usda') + + stage.SetEditTarget(getMetadataDestinationLayer(prim)) + + # Prevent edits. + mayaUsd.lib.registerEditRouter('primMetadata', preventCommandRouter) + + # Try to affect prim via various commands, all should be prevented. + for cmd in ( + usdUfe.ToggleActiveCommand(prim), + usdUfe.ToggleInstanceableCommand(prim), + usdUfe.SetKindCommand(prim, 'group'), + usdUfe.AddPayloadCommand(prim, refFile, True), + usdUfe.AddReferenceCommand(prim, refFile, True), + varSelectionCmd(sceneItem, varSet.GetName(), 'On'), + ): + # Check that the command fails. + with self.assertRaises(RuntimeError): + cmd.execute() + + # Check that nothing was written to the layer. + self.assertTrue(stage.GetEditTarget().GetLayer().empty) + + # Check that no changes were done in any layer. + self.assertTrue(prim.IsActive()) + self.assertFalse(prim.IsInstanceable()) + self.assertEqual(Usd.ModelAPI(prim).GetKind(), '') + self.assertFalse(prim.HasAuthoredPayloads()) + self.assertFalse(prim.HasAuthoredReferences()) + self.assertFalse(varSet.HasAuthoredVariantSelection()) + + def testEditRouterWithFastRouting(self): + ''' + Test metadata fast edit routing. + ''' + self._verifyEditRoutingForMetadata( + lambda item: usdUfe.ToggleActiveCommand(primFromSceneItem(item)), + lambda spec: self.assertFalse(spec.active), + useFastRouting=True, + ) + + def testEditRouterForVariantSelection(self): + ''' + Test edit router functionality for the variant selection commands. + ''' + self._verifyEditRoutingForSetMetadata( + lambda item: varSelectionCmd(item, 'TestVariant', 'On'), + lambda spec: self.assertEqual(dict(spec.variantSelections), {'TestVariant': 'On'}), + ) + + def testEditRouterForSpecificVariantSelection(self): + ''' + Test that edit router can decide a route based on the variantSet + for variant selection editions. + ''' + # Verify that selection in SessionVariant variantSet is correctly routed + # when the callback only routes SessionVariant. + self._verifyEditRoutingForSetMetadata( + lambda item: varSelectionCmd(item, 'SessionVariant', 'On'), + lambda spec: self.assertEqual(dict(spec.variantSelections), {'SessionVariant': 'On'}), + routeFn=routeOnlySessionVariantSelectionToDestLayer, + ) + + # Verify that selection of a another variantSet is NOT routed + # when the callback only routes SessionVariant. + self._verifyEditRoutingForMetadata( + lambda item: varSelectionCmd(item, 'NotSessionVariant', 'On'), + lambda spec: self.assertIsNone(spec), + routeFn=routeOnlySessionVariantSelectionToDestLayer, + ) + + def testEditRouterForToggleActive(self): + ''' + Test edit router functionality for the UsdUfe ToggleActive context op command. + ''' + self._verifyEditRoutingForSetMetadata( + lambda item: usdUfe.ToggleActiveCommand(primFromSceneItem(item)), + lambda spec: self.assertFalse(spec.active), + ) + + def testEditRouterForCustomUndoableCommand(self): + ''' + Test edit router functionality for a custom Python undoable command. + ''' + self._verifyEditRoutingForSetMetadata( + lambda item: CustomSetPrimMetadataCommand(item, 'comment', 'Edited'), + lambda spec: self.assertEqual(spec.comment, 'Edited'), + ) + + def testEditRouterForToggleInstanceable(self): + ''' + Test edit router functionality for the UsdUfe ToggleInstanceable command. + ''' + self._verifyEditRoutingForSetMetadata( + lambda item: usdUfe.ToggleInstanceableCommand(primFromSceneItem(item)), + lambda spec: self.assertTrue(spec.instanceable), + ) + + def testEditRouterForSetKind(self): + ''' + Test edit router functionality for the UsdUfe SetKind command. + ''' + self._verifyEditRoutingForSetMetadata( + lambda item: usdUfe.SetKindCommand(primFromSceneItem(item), 'group'), + lambda spec: self.assertEqual(spec.kind, 'group'), + ) + + def testEditRouterForAddReference(self): + ''' + Test edit router functionality for the UsdUfe AddReference command. + ''' + refFile = testUtils.getTestScene('twoSpheres', 'sphere.usda') + + self._verifyEditRoutingForSetMetadata( + lambda item: usdUfe.AddReferenceCommand(primFromSceneItem(item), refFile, True), + lambda spec: self.assertTrue(spec.hasReferences), + ) + + def testEditRouterForAddPayload(self): + ''' + Test edit router functionality for the UsdUfe AddPayload command. + ''' + refFile = testUtils.getTestScene('twoSpheres', 'sphere.usda') + + self._verifyEditRoutingForSetMetadata( + lambda item: usdUfe.AddPayloadCommand(primFromSceneItem(item), refFile, True), + lambda spec: self.assertTrue(spec.hasPayloads), + ) + + def testEditRouterForClearReferences(self): + ''' + Test edit router functionality for the UsdUfe ClearReferences command. + ''' + refFile = testUtils.getTestScene('twoSpheres', 'sphere.usda') + + self._verifyEditRoutingForClearMetadata( + lambda prim: prim.GetReferences().AddReference(Sdf.Reference(refFile)), + lambda item: usdUfe.ClearReferencesCommand(primFromSceneItem(item)), + lambda spec: self.assertFalse(spec.hasReferences), + ) + + def testEditRouterForClearPayloads(self): + ''' + Test edit router functionality for the UsdUfe ClearPayloads command. + ''' + refFile = testUtils.getTestScene('twoSpheres', 'sphere.usda') + + self._verifyEditRoutingForClearMetadata( + lambda prim: prim.GetPayloads().AddPayload(Sdf.Payload(refFile)), + lambda item: usdUfe.ClearPayloadsCommand(primFromSceneItem(item)), + lambda spec: self.assertFalse(spec.hasPayloads), + ) + + @unittest.skipUnless(ufeMetadataSupported, 'Available only if UFE supports metadata.') + def testEditRouterForSetSceneItemMetadata(self): + ''' + Test edit router functionality for setting Ufe sceneItem metadata. + ''' + self._verifyEditRoutingForSetMetadata( + lambda item: item.setMetadataCmd('Key', 'Edited'), + lambda spec: self.assertEqual(spec.customData.get('Key'), 'Edited'), + ) + + @unittest.skipUnless(ufeMetadataSupported, 'Available only if UFE supports metadata.') + def testEditRouterForSetSceneItemGroupMetadata(self): + ''' + Test edit router functionality for setting Ufe sceneItem metadata. + ''' + self._verifyEditRoutingForSetMetadata( + lambda item: item.setGroupMetadataCmd('Group', 'Key', 'Edited'), + lambda spec: self.assertEqual(spec.customData.get('Group'), {'Key': 'Edited'}), + ) + + @unittest.skipUnless(ufeMetadataSupported, 'Available only if UFE supports metadata.') + def testEditRouterForClearSceneItemMetadata(self): + ''' + Test edit router functionality for clearing Ufe sceneItem metadata. + ''' + self._verifyEditRoutingForClearMetadata( + lambda prim: prim.SetCustomDataByKey('Key', 'Edited'), + lambda item: item.clearMetadataCmd('Key'), + lambda spec: self.assertNotIn('Key', spec.customData), + ) + + @unittest.skipUnless(ufeMetadataSupported, 'Available only if UFE supports metadata.') + def testEditRouterForClearSceneItemGroupMetadata(self): + ''' + Test edit router functionality for clearing Ufe sceneItem metadata. + ''' + self._verifyEditRoutingForClearMetadata( + lambda prim: prim.SetCustomDataByKey('Group:Key', 'Edited'), + lambda item: item.clearGroupMetadataCmd('Group', 'Key'), + lambda spec: self.assertNotIn('Group', spec.customData), + ) + + @unittest.skipUnless(ufeMetadataSupported, 'Available only if UFE supports metadata.') + def testEditRouterForSetSceneItemSessionMetadata(self): + ''' + Test edit router functionality for setting Ufe sceneItem metadata with 'SessionLayer-' + group prefix. They are authored in session layer by default, verify that we can + route them to another layer. + ''' + self._verifyEditRoutingForSetMetadata( + lambda item: item.setGroupMetadataCmd('SessionLayer-Autodesk', 'Key', 'Edited'), + lambda spec: self.assertEqual(spec.customData.get('Autodesk'), {'Key': 'Edited'}), + ) + + @unittest.skipUnless(ufeMetadataSupported, 'Available only if UFE supports metadata.') + def testEditRouterForClearSceneItemSessionMetadata(self): + ''' + Test edit router functionality for clearing Ufe sceneItem metadata with 'SessionLayer-' + group prefix. They are authored in session layer by default, verify that we can + route them to another layer. + ''' + self._verifyEditRoutingForClearMetadata( + lambda prim: prim.SetCustomDataByKey('Autodesk:Key', 'Edited'), + lambda item: item.clearGroupMetadataCmd('SessionLayer-Autodesk', 'Key'), + lambda spec: self.assertNotIn('Autodesk', spec.customData), + ) + +if __name__ == '__main__': + unittest.main(verbosity=2)