diff --git a/lib/ufe/StagesSubject.cpp b/lib/ufe/StagesSubject.cpp index 48f8db0961..033f843fd9 100644 --- a/lib/ufe/StagesSubject.cpp +++ b/lib/ufe/StagesSubject.cpp @@ -20,8 +20,11 @@ #include "ProxyShapeHandler.h" #include "private/InPathChange.h" -#include +#ifdef UFE_V2_FEATURES_AVAILABLE +#include +#endif #include +#include #include #include #include @@ -31,6 +34,25 @@ #include +#ifdef UFE_V2_FEATURES_AVAILABLE +#include + +namespace { + +// The attribute change notification guard is not meant to be nested, but +// use a counter nonetheless to provide consistent behavior in such cases. +int attributeChangedNotificationGuardCount = 0; + +bool inAttributeChangedNotificationGuard() +{ + return attributeChangedNotificationGuardCount > 0; +} + +std::unordered_map pendingAttributeChangedNotifications; + +} +#endif + MAYAUSD_NS_DEF { namespace ufe { @@ -147,12 +169,12 @@ void StagesSubject::afterOpen() me, &StagesSubject::stageChanged, stage); } - // Set up our stage to AL_usdmaya_ProxyShape UFE path (and reverse) + // Set up our stage to proxy shape UFE path (and reverse) // mapping. We do this with the following steps: // - get all proxyShape nodes in the scene. - // - get their AL Python wrapper - // - get their Dag paths + // - get their Dag paths. // - convert the Dag paths to UFE paths. + // - get their stage. g_StageMap.clear(); auto proxyShapeNames = ProxyShapeHandler::getAllNames(); for (const auto& psn : proxyShapeNames) @@ -207,6 +229,22 @@ void StagesSubject::stageChanged(UsdNotice::ObjectsChanged const& notice, UsdSta { auto usdPrimPathStr = changedPath.GetPrimPath().GetString(); auto ufePath = stagePath(sender) + Ufe::PathSegment(usdPrimPathStr, g_USDRtid, '/'); + +#ifdef UFE_V2_FEATURES_AVAILABLE + // isPrimPropertyPath() does not consider relational attributes + // isPropertyPath() does consider relational attributes + // isRelationalAttributePath() considers only relational attributes + if (changedPath.IsPrimPropertyPath()) { + if (inAttributeChangedNotificationGuard()) { + pendingAttributeChangedNotifications[ufePath] = + changedPath.GetName(); + } + else { + Ufe::Attributes::notify(ufePath, changedPath.GetName()); + } + } +#endif + // We need to determine if the change is a Transform3d change. // We must at least pick up xformOp:translate, xformOp:rotateXYZ, // and xformOp:scale. @@ -223,5 +261,39 @@ void StagesSubject::onStageSet(const UsdMayaProxyStageSetNotice& notice) afterOpen(); } +#ifdef UFE_V2_FEATURES_AVAILABLE +AttributeChangedNotificationGuard::AttributeChangedNotificationGuard() +{ + if (inAttributeChangedNotificationGuard()) { + TF_CODING_ERROR("Attribute changed notification guard cannot be nested."); + } + + if (attributeChangedNotificationGuardCount == 0 && + !pendingAttributeChangedNotifications.empty()) { + TF_CODING_ERROR("Stale pending attribute changed notifications."); + } + + ++attributeChangedNotificationGuardCount; + +} + +AttributeChangedNotificationGuard::~AttributeChangedNotificationGuard() +{ + if (--attributeChangedNotificationGuardCount < 0) { + TF_CODING_ERROR("Corrupt attribute changed notification guard."); + } + + if (attributeChangedNotificationGuardCount > 0 ) { + return; + } + + for (const auto& notificationInfo : pendingAttributeChangedNotifications) { + Ufe::Attributes::notify(notificationInfo.first, notificationInfo.second); + } + + pendingAttributeChangedNotifications.clear(); +} +#endif + } // namespace ufe } // namespace MayaUsd diff --git a/lib/ufe/StagesSubject.h b/lib/ufe/StagesSubject.h index 3a3587ed4d..840658398f 100644 --- a/lib/ufe/StagesSubject.h +++ b/lib/ufe/StagesSubject.h @@ -26,6 +26,8 @@ #include +#include // For UFE_V2_FEATURES_AVAILABLE + PXR_NAMESPACE_USING_DIRECTIVE MAYAUSD_NS_DEF { @@ -86,5 +88,29 @@ class MAYAUSD_CORE_PUBLIC StagesSubject : public TfWeakBase }; // StagesSubject +#ifdef UFE_V2_FEATURES_AVAILABLE +//! \brief Guard to delay attribute changed notifications. +/*! + Instantiating an object of this class allows the attribute changed + notifications to be delayed until the guard expires. + + The guard collapses down notifications for a given UFE path, which is + desirable to avoid duplicate notifications. However, it is an error to + have notifications for more than one attribute within a single guard. + */ +class MAYAUSD_CORE_PUBLIC AttributeChangedNotificationGuard { +public: + + AttributeChangedNotificationGuard(); + ~AttributeChangedNotificationGuard(); + + //@{ + //! Cannot be copied or assigned. + AttributeChangedNotificationGuard(const AttributeChangedNotificationGuard&) = delete; + const AttributeChangedNotificationGuard& operator&(const AttributeChangedNotificationGuard&) = delete; + //@} +}; +#endif + } // namespace ufe } // namespace MayaUsd diff --git a/lib/ufe/UsdAttribute.cpp b/lib/ufe/UsdAttribute.cpp index 991307281f..50456c5f7f 100644 --- a/lib/ufe/UsdAttribute.cpp +++ b/lib/ufe/UsdAttribute.cpp @@ -15,6 +15,7 @@ // #include "UsdAttribute.h" +#include "StagesSubject.h" #include #include @@ -47,6 +48,36 @@ static constexpr char kErrorMsgEnumNoValue[] = "Enum string attribute has no val namespace { +template +bool setUsdAttr(const PXR_NS::UsdAttribute& attr, const T& value) +{ + // As of 24-Nov-2019, calling Set() on a UsdAttribute causes two "info only" + // change notifications to be sent (see StagesSubject::stageChanged). With + // the current USD implementation (USD 19.11), UsdAttribute::Set() ends up + // in UsdStage::_SetValueImpl(). This function calls in sequence: + // - UsdStage::_CreateAttributeSpecForEditing(), which has an SdfChangeBlock + // whose expiry causes a notification to be sent. + // - SdfLayer::SetField(), which also has an SdfChangeBlock whose + // expiry causes a notification to be sent. + // These two calls appear to be made on all calls to UsdAttribute::Set(), + // not just on the first call. + // + // Trying to wrap the call to UsdAttribute::Set() inside an additional + // SdfChangeBlock fails: no notifications are sent at all. This is most + // likely because of the warning given in the SdfChangeBlock documentation: + // + // https://graphics.pixar.com/usd/docs/api/class_sdf_change_block.html + // + // which stages that "it is not safe to use [...] [a] downstream API [such + // as Usd] while a changeblock is open [...]". + // + // Therefore, we have implemented an attribute change block notification of + // our own in the StagesSubject, which we invoke here, so that only a + // single UFE attribute changed notification is generated. + MayaUsd::ufe::AttributeChangedNotificationGuard guard; + return attr.Set(value); +} + std::string getUsdAttributeValueAsString(const PXR_NS::UsdAttribute& attr) { if (!attr.HasValue()) return std::string(); @@ -92,7 +123,7 @@ void setUsdAttributeVectorFromUfe(PXR_NS::UsdAttribute& attr, const U& value) T vec; UFE_ASSERT_MSG(attr.Get(&vec), kErrorMsgInvalidType); vec.Set(value.x(), value.y(), value.z()); - bool b = attr.Set(vec); + bool b = setUsdAttr(attr, vec); UFE_ASSERT_MSG(b, kErrorMsgFailedSet); } @@ -202,7 +233,7 @@ void UsdAttributeEnumString::set(const std::string& value) PXR_NS::TfToken dummy; UFE_ASSERT_MSG(fUsdAttr.Get(&dummy), kErrorMsgInvalidType); PXR_NS::TfToken tok(value); - bool b = fUsdAttr.Set(tok); + bool b = setUsdAttr(fUsdAttr, tok); UFE_ASSERT_MSG(b, kErrorMsgFailedSet); } @@ -269,7 +300,7 @@ void TypedUsdAttribute::set(const std::string& value) { std::string dummy; UFE_ASSERT_MSG(fUsdAttr.Get(&dummy), kErrorMsgInvalidType); - bool b = fUsdAttr.Set(value); + bool b = setUsdAttr(fUsdAttr, value); UFE_ASSERT_MSG(b, kErrorMsgFailedSet); return; } @@ -278,7 +309,7 @@ void TypedUsdAttribute::set(const std::string& value) PXR_NS::TfToken dummy; UFE_ASSERT_MSG(fUsdAttr.Get(&dummy), kErrorMsgInvalidType); PXR_NS::TfToken tok(value); - bool b = fUsdAttr.Set(tok); + bool b = setUsdAttr(fUsdAttr, tok); UFE_ASSERT_MSG(b, kErrorMsgFailedSet); return; } @@ -300,7 +331,7 @@ void TypedUsdAttribute::set(const Ufe::Color3f& value) GfVec3f vec; UFE_ASSERT_MSG(fUsdAttr.Get(&vec), kErrorMsgInvalidType); vec.Set(value.r(), value.g(), value.b()); - bool b = fUsdAttr.Set(vec); + bool b = setUsdAttr(fUsdAttr, vec); UFE_ASSERT_MSG(b, kErrorMsgFailedSet); } @@ -360,7 +391,7 @@ void TypedUsdAttribute::set(const T& value) { T dummy; UFE_ASSERT_MSG(fUsdAttr.Get(&dummy), kErrorMsgInvalidType); - bool b = fUsdAttr.Set(value); + bool b = setUsdAttr(fUsdAttr, value); UFE_ASSERT_MSG(b, kErrorMsgFailedSet); } @@ -481,7 +512,7 @@ bool UsdAttribute::setValue(const std::string& value) if (!cast.IsEmpty()) cast.Swap(val); - return fUsdAttr.Set(val); + return setUsdAttr(fUsdAttr, val); } #endif diff --git a/test/lib/ufe/test-samples/ballset/StandaloneScene/top_layer.ma b/test/lib/ufe/test-samples/ballset/StandaloneScene/top_layer.ma index 2b042039dd..fa66252b63 100644 --- a/test/lib/ufe/test-samples/ballset/StandaloneScene/top_layer.ma +++ b/test/lib/ufe/test-samples/ballset/StandaloneScene/top_layer.ma @@ -113,7 +113,7 @@ createNode script -n "uiConfigurationScriptNode"; + " -width 1\n -height 1\n -sceneRenderFilter 0\n $editorName;\n modelEditor -e -viewSelected 0 $editorName;\n\t\tif (!$useSceneConfig) {\n\t\t\tpanel -e -l $label $panelName;\n\t\t}\n\t}\n\n\n\t$panelName = `sceneUIReplacement -getNextPanel \"modelPanel\" (localizedPanelLabel(\"Persp View\")) `;\n\tif (\"\" != $panelName) {\n\t\t$label = `panel -q -label $panelName`;\n\t\tmodelPanel -edit -l (localizedPanelLabel(\"Persp View\")) -mbv $menusOkayInPanels $panelName;\n\t\t$editorName = $panelName;\n modelEditor -e \n -camera \"persp\" \n -useInteractiveMode 0\n -displayLights \"default\" \n -displayAppearance \"smoothShaded\" \n -activeOnly 0\n -ignorePanZoom 0\n -wireframeOnShaded 0\n -headsUpDisplay 1\n -holdOuts 1\n -selectionHiliteDisplay 1\n -useDefaultMaterial 0\n -bufferMode \"double\" \n -twoSidedLighting 0\n -backfaceCulling 0\n -xray 0\n -jointXray 0\n" + " -activeComponentsXray 0\n -displayTextures 0\n -smoothWireframe 0\n -lineWidth 1\n -textureAnisotropic 0\n -textureHilight 1\n -textureSampling 2\n -textureDisplay \"modulate\" \n -textureMaxSize 16384\n -fogging 0\n -fogSource \"fragment\" \n -fogMode \"linear\" \n -fogStart 0\n -fogEnd 100\n -fogDensity 0.1\n -fogColor 0.5 0.5 0.5 1 \n -depthOfFieldPreview 1\n -maxConstantTransparency 1\n -rendererName \"vp2Renderer\" \n -objectFilterShowInHUD 1\n -isFiltered 0\n -colorResolution 256 256 \n -bumpResolution 512 512 \n -textureCompression 0\n -transparencyAlgorithm \"frontAndBackCull\" \n -transpInShadows 0\n -cullingOverride \"none\" \n -lowQualityLighting 0\n -maximumNumHardwareLights 1\n -occlusionCulling 0\n -shadingModel 0\n" + " -useBaseRenderer 0\n -useReducedRenderer 0\n -smallObjectCulling 0\n -smallObjectThreshold -1 \n -interactiveDisableShadows 0\n -interactiveBackFaceCull 0\n -sortTransparent 1\n -controllers 1\n -nurbsCurves 1\n -nurbsSurfaces 1\n -polymeshes 1\n -subdivSurfaces 1\n -planes 1\n -lights 1\n -cameras 1\n -controlVertices 1\n -hulls 1\n -grid 1\n -imagePlane 1\n -joints 1\n -ikHandles 1\n -deformers 1\n -dynamics 1\n -particleInstancers 1\n -fluids 1\n -hairSystems 1\n -follicles 1\n -nCloths 1\n -nParticles 1\n -nRigids 1\n -dynamicConstraints 1\n -locators 1\n -manipulators 1\n -pluginShapes 1\n -dimensions 1\n -handles 1\n -pivots 1\n -textures 1\n" - + " -strokes 1\n -motionTrails 1\n -clipGhosts 1\n -greasePencils 1\n -shadows 0\n -captureSequenceNumber -1\n -width 1125\n -height 825\n -sceneRenderFilter 0\n $editorName;\n modelEditor -e -viewSelected 0 $editorName;\n\t\tif (!$useSceneConfig) {\n\t\t\tpanel -e -l $label $panelName;\n\t\t}\n\t}\n\n\n\t$panelName = `sceneUIReplacement -getNextPanel \"outlinerPanel\" (localizedPanelLabel(\"ToggledOutliner\")) `;\n\tif (\"\" != $panelName) {\n\t\t$label = `panel -q -label $panelName`;\n\t\toutlinerPanel -edit -l (localizedPanelLabel(\"ToggledOutliner\")) -mbv $menusOkayInPanels $panelName;\n\t\t$editorName = $panelName;\n outlinerEditor -e \n -docTag \"isolOutln_fromSeln\" \n -showShapes 0\n -showAssignedMaterials 0\n -showTimeEditor 1\n -showReferenceNodes 0\n -showReferenceMembers 0\n -showAttributes 0\n -showConnected 0\n -showAnimCurvesOnly 0\n -showMuteInfo 0\n" + + " -strokes 1\n -motionTrails 1\n -clipGhosts 1\n -greasePencils 1\n -shadows 0\n -captureSequenceNumber -1\n -width 1125\n -height 825\n -sceneRenderFilter 0\n $editorName;\n modelEditor -e -viewSelected 0 $editorName;\n\t\tif (!$useSceneConfig) {\n\t\t\tpanel -e -l $label $panelName;\n\t\t}\n\t}\n\n\n\t$panelName = `sceneUIReplacement -getNextPanel \"outlinerPanel\" (localizedPanelLabel(\"ToggledOutliner\")) `;\n\tif (\"\" != $panelName) {\n\t\t$label = `panel -q -label $panelName`;\n\t\toutlinerPanel -edit -l (localizedPanelLabel(\"ToggledOutliner\")) -mbv $menusOkayInPanels $panelName;\n\t\t$editorName = $panelName;\n outlinerEditor -e \n -docTag \"isolOutln_fromSeln\" \n -showShapes 1\n -showAssignedMaterials 0\n -showTimeEditor 1\n -showReferenceNodes 0\n -showReferenceMembers 0\n -showAttributes 0\n -showConnected 0\n -showAnimCurvesOnly 0\n -showMuteInfo 0\n" + " -organizeByLayer 1\n -organizeByClip 1\n -showAnimLayerWeight 1\n -autoExpandLayers 1\n -autoExpand 0\n -showDagOnly 1\n -showAssets 1\n -showContainedOnly 1\n -showPublishedAsConnected 0\n -showParentContainers 0\n -showContainerContents 1\n -ignoreDagHierarchy 0\n -expandConnections 0\n -showUpstreamCurves 1\n -showUnitlessCurves 1\n -showCompounds 1\n -showLeafs 1\n -showNumericAttrsOnly 0\n -highlightActive 1\n -autoSelectNewObjects 0\n -doNotSelectNewObjects 0\n -dropIsParent 1\n -transmitFilters 0\n -setFilter \"defaultSetFilter\" \n -showSetMembers 1\n -allowMultiSelection 1\n -alwaysToggleSelect 0\n -directSelect 0\n -isSet 0\n -isSetMember 0\n -displayMode \"DAG\" \n -expandObjects 0\n -setsIgnoreFilters 1\n" + " -containersIgnoreFilters 0\n -editAttrName 0\n -showAttrValues 0\n -highlightSecondary 0\n -showUVAttrsOnly 0\n -showTextureNodesOnly 0\n -attrAlphaOrder \"default\" \n -animLayerFilterOptions \"allAffecting\" \n -sortOrder \"none\" \n -longNames 0\n -niceNames 1\n -showNamespace 1\n -showPinIcons 0\n -mapMotionTrails 0\n -ignoreHiddenAttribute 0\n -ignoreOutlinerColor 0\n -renderFilterVisible 0\n -renderFilterIndex 0\n -selectionOrder \"chronological\" \n -expandAttribute 0\n $editorName;\n\t\tif (!$useSceneConfig) {\n\t\t\tpanel -e -l $label $panelName;\n\t\t}\n\t}\n\n\n\t$panelName = `sceneUIReplacement -getNextPanel \"outlinerPanel\" (localizedPanelLabel(\"Outliner\")) `;\n\tif (\"\" != $panelName) {\n\t\t$label = `panel -q -label $panelName`;\n\t\toutlinerPanel -edit -l (localizedPanelLabel(\"Outliner\")) -mbv $menusOkayInPanels $panelName;\n" + "\t\t$editorName = $panelName;\n outlinerEditor -e \n -showShapes 0\n -showAssignedMaterials 0\n -showTimeEditor 1\n -showReferenceNodes 0\n -showReferenceMembers 0\n -showAttributes 0\n -showConnected 0\n -showAnimCurvesOnly 0\n -showMuteInfo 0\n -organizeByLayer 1\n -organizeByClip 1\n -showAnimLayerWeight 1\n -autoExpandLayers 1\n -autoExpand 0\n -showDagOnly 1\n -showAssets 1\n -showContainedOnly 1\n -showPublishedAsConnected 0\n -showParentContainers 0\n -showContainerContents 1\n -ignoreDagHierarchy 0\n -expandConnections 0\n -showUpstreamCurves 1\n -showUnitlessCurves 1\n -showCompounds 1\n -showLeafs 1\n -showNumericAttrsOnly 0\n -highlightActive 1\n -autoSelectNewObjects 0\n -doNotSelectNewObjects 0\n -dropIsParent 1\n" diff --git a/test/lib/ufe/testAttribute.py b/test/lib/ufe/testAttribute.py index ff1fe5877c..b3e345eaf3 100644 --- a/test/lib/ufe/testAttribute.py +++ b/test/lib/ufe/testAttribute.py @@ -22,8 +22,24 @@ from pxr import UsdGeom import random +import maya.cmds as cmds +import maya.internal.ufeSupport.ufeCmdWrapper as ufeCmd + import unittest +class TestObserver(ufe.Observer): + def __init__(self): + super(TestObserver, self).__init__() + self._notifications = 0 + + def __call__(self, notification): + if isinstance(notification, ufe.AttributeChanged): + self._notifications += 1 + + @property + def notifications(self): + return self._notifications + class AttributeTestCase(unittest.TestCase): '''Verify the Attribute UFE interface, for multiple runtimes. ''' @@ -40,6 +56,11 @@ def setUpClass(cls): random.seed() + @classmethod + def tearDownClass(cls): + # See comments in MayaUFEPickWalkTesting.tearDownClass + cmds.file(new=True, force=True) + def setUp(self): '''Called initially to set up the maya test environment''' self.assertTrue(self.pluginsLoaded) @@ -52,6 +73,23 @@ def assertColorAlmostEqual(self, ufeColor, usdColor): for va, vb in zip(ufeColor.color, usdColor): self.assertAlmostEqual(va, vb, places=6) + def runUndoRedo(self, attr, newVal, decimalPlaces=None): + oldVal = attr.get() + assert oldVal != newVal, "Undo / redo testing requires setting a value different from the current value" + + ufeCmd.execute(attr.setCmd(newVal)) + + if decimalPlaces is not None: + self.assertAlmostEqual(attr.get(), newVal, decimalPlaces) + newVal = attr.get() + else: + self.assertEqual(attr.get(), newVal) + + cmds.undo() + self.assertEqual(attr.get(), oldVal) + cmds.redo() + self.assertEqual(attr.get(), newVal) + def runTestAttribute(self, path, attrName, ufeAttrClass, ufeAttrType): '''Engine method to run attribute test.''' @@ -143,6 +181,9 @@ def testAttributeEnumString(self): # Verify that the new UFE value matches what is directly in USD. self.assertEqual(ufeAttr.get(), usdAttr.Get()) + # Change back to 'inherited' using a command. + self.runUndoRedo(ufeAttr, UsdGeom.Tokens.inherited) + def testAttributeBool(self): '''Test the Bool attribute type.''' @@ -166,6 +207,8 @@ def testAttributeBool(self): # Then make sure that new UFE value matches what it in USD. self.assertEqual(ufeAttr.get(), usdAttr.Get()) + self.runUndoRedo(ufeAttr, not ufeAttr.get()) + def testAttributeInt(self): '''Test the Int attribute type.''' @@ -189,6 +232,8 @@ def testAttributeInt(self): # Then make sure that new UFE value matches what it in USD. self.assertEqual(ufeAttr.get(), usdAttr.Get()) + self.runUndoRedo(ufeAttr, ufeAttr.get()+1) + def testAttributeFloat(self): '''Test the Float attribute type.''' @@ -212,6 +257,11 @@ def testAttributeFloat(self): # Then make sure that new UFE value matches what it in USD. self.assertEqual(ufeAttr.get(), usdAttr.Get()) + # Python floating-point numbers are doubles. If stored in a float + # attribute, the resulting precision will be less than the original + # Python value. + self.runUndoRedo(ufeAttr, ufeAttr.get() + 1.0, decimalPlaces=6) + def _testAttributeDouble(self): '''Test the Double attribute type.''' @@ -242,6 +292,8 @@ def testAttributeStringString(self): # Then make sure that new UFE value matches what it in USD. self.assertEqual(ufeAttr.get(), usdAttr.Get()) + self.runUndoRedo(ufeAttr, 'potato') + def testAttributeStringToken(self): '''Test the String (Token) attribute type.''' @@ -266,6 +318,8 @@ def testAttributeStringToken(self): # Then make sure that new UFE value matches what it in USD. self.assertEqual(ufeAttr.get(), usdAttr.Get()) + self.runUndoRedo(ufeAttr, 'Box') + def testAttributeColorFloat3(self): '''Test the ColorFloat3 attribute type.''' @@ -290,6 +344,13 @@ def testAttributeColorFloat3(self): # Then make sure that new UFE value matches what it in USD. self.assertColorAlmostEqual(ufeAttr.get(), usdAttr.Get()) + # The following causes a segmentation fault on CentOS 7. + # self.runUndoRedo(ufeAttr, + # ufe.Color3f(vec.r()+1.0, vec.g()+2.0, vec.b()+3.0)) + # Entered as MAYA-102168. + newVec = ufe.Color3f(vec.color[0]+1.0, vec.color[1]+2.0, vec.color[2]+3.0) + self.runUndoRedo(ufeAttr, newVec) + def _testAttributeInt3(self): '''Test the Int3 attribute type.''' @@ -313,13 +374,16 @@ def testAttributeFloat3(self): # Compare the initial UFE value to that directly from USD. self.assertVectorAlmostEqual(ufeAttr.get(), usdAttr.Get()) - # Set the attribute in UFE with some random color values. + # Set the attribute in UFE with some random values. vec = ufe.Vector3f(random.random(), random.random(), random.random()) ufeAttr.set(vec) # Then make sure that new UFE value matches what it in USD. self.assertVectorAlmostEqual(ufeAttr.get(), usdAttr.Get()) + self.runUndoRedo(ufeAttr, + ufe.Vector3f(vec.x()+1.0, vec.y()+2.0, vec.z()+3.0)) + def testAttributeDouble3(self): '''Test the Double3 attribute type.''' @@ -343,3 +407,148 @@ def testAttributeDouble3(self): # Then make sure that new UFE value matches what it in USD. self.assertVectorAlmostEqual(ufeAttr.get(), usdAttr.Get()) + + self.runUndoRedo(ufeAttr, + ufe.Vector3d(vec.x()-1.0, vec.y()-2.0, vec.z()-3.0)) + + def testObservation(self): + '''Test Attributes observation interface. + + Test both global attribute observation and per-node attribute + observation. + ''' + + # Create three observers, one for global attribute observation, and two + # on different UFE items. + proxyShapePathSegment = mayaUtils.createUfePathSegment( + "|world|transform1|proxyShape1") + path = ufe.Path([ + proxyShapePathSegment, + usdUtils.createUfePathSegment('/Room_set/Props/Ball_34')]) + ball34 = ufe.Hierarchy.createItem(path) + path = ufe.Path([ + proxyShapePathSegment, + usdUtils.createUfePathSegment('/Room_set/Props/Ball_35')]) + ball35 = ufe.Hierarchy.createItem(path) + + (ball34Obs, ball35Obs, globalObs) = [TestObserver() for i in range(3)] + + # Maya registers a single global observer on startup. + self.assertEqual(ufe.Attributes.nbObservers(), 1) + + # No item-specific observers. + self.assertFalse(ufe.Attributes.hasObservers(ball34.path())) + self.assertFalse(ufe.Attributes.hasObservers(ball35.path())) + self.assertEqual(ufe.Attributes.nbObservers(ball34), 0) + self.assertEqual(ufe.Attributes.nbObservers(ball35), 0) + self.assertFalse(ufe.Attributes.hasObserver(ball34, ball34Obs)) + self.assertFalse(ufe.Attributes.hasObserver(ball35, ball35Obs)) + + # No notifications yet. + self.assertEqual(ball34Obs.notifications, 0) + self.assertEqual(ball35Obs.notifications, 0) + self.assertEqual(globalObs.notifications, 0) + + # Add a global observer. + ufe.Attributes.addObserver(globalObs) + + self.assertEqual(ufe.Attributes.nbObservers(), 2) + self.assertFalse(ufe.Attributes.hasObservers(ball34.path())) + self.assertFalse(ufe.Attributes.hasObservers(ball35.path())) + self.assertEqual(ufe.Attributes.nbObservers(ball34), 0) + self.assertEqual(ufe.Attributes.nbObservers(ball35), 0) + self.assertFalse(ufe.Attributes.hasObserver(ball34, ball34Obs)) + self.assertFalse(ufe.Attributes.hasObserver(ball35, ball35Obs)) + + # Add item-specific observers. + ufe.Attributes.addObserver(ball34, ball34Obs) + + self.assertEqual(ufe.Attributes.nbObservers(), 2) + self.assertTrue(ufe.Attributes.hasObservers(ball34.path())) + self.assertFalse(ufe.Attributes.hasObservers(ball35.path())) + self.assertEqual(ufe.Attributes.nbObservers(ball34), 1) + self.assertEqual(ufe.Attributes.nbObservers(ball35), 0) + self.assertTrue(ufe.Attributes.hasObserver(ball34, ball34Obs)) + self.assertFalse(ufe.Attributes.hasObserver(ball34, ball35Obs)) + self.assertFalse(ufe.Attributes.hasObserver(ball35, ball35Obs)) + + ufe.Attributes.addObserver(ball35, ball35Obs) + + self.assertTrue(ufe.Attributes.hasObservers(ball35.path())) + self.assertEqual(ufe.Attributes.nbObservers(ball34), 1) + self.assertEqual(ufe.Attributes.nbObservers(ball35), 1) + self.assertTrue(ufe.Attributes.hasObserver(ball35, ball35Obs)) + self.assertFalse(ufe.Attributes.hasObserver(ball35, ball34Obs)) + + # Make a change to ball34, global and ball34 observers change. + ball34Attrs = ufe.Attributes.attributes(ball34) + ball34XlateAttr = ball34Attrs.attribute('xformOp:translate') + + self.assertEqual(ball34Obs.notifications, 0) + + ufeCmd.execute(ball34XlateAttr.setCmd(ufe.Vector3d(1, 2, 3))) + + self.assertEqual(ball34Obs.notifications, 1) + self.assertEqual(ball35Obs.notifications, 0) + self.assertEqual(globalObs.notifications, 1) + + # Undo, redo + cmds.undo() + + self.assertEqual(ball34Obs.notifications, 2) + self.assertEqual(ball35Obs.notifications, 0) + self.assertEqual(globalObs.notifications, 2) + + cmds.redo() + + self.assertEqual(ball34Obs.notifications, 3) + self.assertEqual(ball35Obs.notifications, 0) + self.assertEqual(globalObs.notifications, 3) + + # Make a change to ball35, global and ball35 observers change. + ball35Attrs = ufe.Attributes.attributes(ball35) + ball35XlateAttr = ball35Attrs.attribute('xformOp:translate') + + ufeCmd.execute(ball35XlateAttr.setCmd(ufe.Vector3d(1, 2, 3))) + + self.assertEqual(ball34Obs.notifications, 3) + self.assertEqual(ball35Obs.notifications, 1) + self.assertEqual(globalObs.notifications, 4) + + # Undo, redo + cmds.undo() + + self.assertEqual(ball34Obs.notifications, 3) + self.assertEqual(ball35Obs.notifications, 2) + self.assertEqual(globalObs.notifications, 5) + + cmds.redo() + + self.assertEqual(ball34Obs.notifications, 3) + self.assertEqual(ball35Obs.notifications, 3) + self.assertEqual(globalObs.notifications, 6) + + # Test removeObserver. + ufe.Attributes.removeObserver(ball34, ball34Obs) + + self.assertFalse(ufe.Attributes.hasObservers(ball34.path())) + self.assertTrue(ufe.Attributes.hasObservers(ball35.path())) + self.assertEqual(ufe.Attributes.nbObservers(ball34), 0) + self.assertEqual(ufe.Attributes.nbObservers(ball35), 1) + self.assertFalse(ufe.Attributes.hasObserver(ball34, ball34Obs)) + + ufeCmd.execute(ball34XlateAttr.setCmd(ufe.Vector3d(4, 5, 6))) + + self.assertEqual(ball34Obs.notifications, 3) + self.assertEqual(ball35Obs.notifications, 3) + self.assertEqual(globalObs.notifications, 7) + + ufe.Attributes.removeObserver(globalObs) + + self.assertEqual(ufe.Attributes.nbObservers(), 1) + + ufeCmd.execute(ball34XlateAttr.setCmd(ufe.Vector3d(7, 8, 9))) + + self.assertEqual(ball34Obs.notifications, 3) + self.assertEqual(ball35Obs.notifications, 3) + self.assertEqual(globalObs.notifications, 7) diff --git a/test/lib/ufe/ufeTestUtils/mayaUtils.py b/test/lib/ufe/ufeTestUtils/mayaUtils.py index fe71f3dad6..258697f02f 100644 --- a/test/lib/ufe/ufeTestUtils/mayaUtils.py +++ b/test/lib/ufe/ufeTestUtils/mayaUtils.py @@ -26,8 +26,6 @@ import ufe -ALL_PLUGINS = ["mayaUsdPlugin", "ufeTestCmdsPlugin"] - mayaRuntimeID = 1 mayaSeparator = "|" @@ -64,9 +62,15 @@ def isMayaUsdPluginLoaded(): Returns: True if plugins loaded successfully. False if a plugin failed to load """ - successLoad = True - for plugin in ALL_PLUGINS: - successLoad = successLoad and loadPlugin(plugin) + # Load the mayaUsdPlugin first. + if not loadPlugin("mayaUsdPlugin"): + return False + + # Load the UFE support plugin, for ufeSelectCmd support. If this plugin + # isn't included in the distribution of Maya (e.g. Maya 2019 or 2020), use + # fallback test plugin. + if not (loadPlugin("ufeSupport") or loadPlugin("ufeTestCmdsPlugin")): + return False # The renderSetup Python plugin registers a file new callback to Maya. On # test application exit (in TbaseApp::cleanUp()), a file new is done and @@ -76,9 +80,9 @@ def isMayaUsdPluginLoaded(): rs = 'renderSetup' if cmds.pluginInfo(rs, q=True, loaded=True): unloaded = cmds.unloadPlugin(rs) - successLoad = successLoad and (unloaded[0] == rs) + return (unloaded[0] == rs) - return successLoad + return True def createUfePathSegment(mayaPath): """ diff --git a/test/lib/ufe/ufeTestUtils/ufeUtils.py b/test/lib/ufe/ufeTestUtils/ufeUtils.py index 383d3648cc..de0d36ce3f 100644 --- a/test/lib/ufe/ufeTestUtils/ufeUtils.py +++ b/test/lib/ufe/ufeTestUtils/ufeUtils.py @@ -22,7 +22,11 @@ import ufe from maya import cmds -from ufeScripts import ufeSelectCmd +try: + from maya.internal.ufeSupport import ufeSelectCmd +except ImportError: + # Maya 2019 and 2020 don't have ufeSupport plugin, so use fallback. + from ufeScripts import ufeSelectCmd def getUfeGlobalSelectionList(): """ @@ -50,4 +54,4 @@ def selectPath(path, replace=False): selection.append(sceneItem) ufeSelectCmd.replaceWith(selection) else: - ufeSelectCmd.append(sceneItem) \ No newline at end of file + ufeSelectCmd.append(sceneItem)