From e1630a24c75b6d572c5a42e24749e3402e3bccbe Mon Sep 17 00:00:00 2001 From: Pierre Baillargeon Date: Tue, 22 Oct 2024 13:10:17 -0400 Subject: [PATCH 1/3] EMSUSD-1521 modify AE section order - Accumulate all schemas and their attributes before creating any UI for them. - Re-ordered schemas according to the design. - Renamed Xformable to Transforms. - Made all sections be collapsed by default except the prim type section, Light and Light type - Removed the "Applied Schemas" section. - Add a unit test and fix some tests that assumed sections would be expanded. - Make all sections (except materials, which has a special logic) be re-orderable in the future. - Update the documentation to reflect the changes. --- .../Attribute-Editor-Template-Doc.md | 70 ++++-- .../resources/ae/usdschemabase/ae_template.py | 223 ++++++++++++------ .../scripts/mayaUsdLibRegisterStrings.py | 2 +- test/lib/testAttributeEditorTemplate.py | 207 ++++++++++++---- 4 files changed, 368 insertions(+), 134 deletions(-) diff --git a/lib/mayaUsd/resources/ae/usdschemabase/Attribute-Editor-Template-Doc.md b/lib/mayaUsd/resources/ae/usdschemabase/Attribute-Editor-Template-Doc.md index 98178aef78..0dfa1a29aa 100644 --- a/lib/mayaUsd/resources/ae/usdschemabase/Attribute-Editor-Template-Doc.md +++ b/lib/mayaUsd/resources/ae/usdschemabase/Attribute-Editor-Template-Doc.md @@ -22,28 +22,41 @@ The AE template has two functions to manage supressed attributes: `suppress` supresses the given attribute. `suppressArrayAttribute` which supresses all array attribute if the option to display is turned off. -The constructor of the AE template calls `buildUI` to fit each attribute in a -custom control. Afterward it calls the functions (in order) to create some UI sections that are always present: -* `createAppliedSchemasSection` -* `createCustomCallbackSection` - see below for more info -* `createCustomExtraAttrs` -* `createMetadataSection` - -The `buildUI` function goes through each USD schema of the USD prim. If the -schema is one of the "well-known" ones that have a specialized UI section, -then that section gets created. For example, shader, transform and display each -have a specialized UI section. - -If the schema is not a special one, then `buildUI` retrieves all attributes of -the schema and calls `createSection` to create the UI section that will contain -those attributes. `createSection` checks if the attribute is not supressed and -calls `addControls` to do the UI creation. - -The `addControls` function goes through each custom control and ask each of them -to create the UI for the attributes. Once a custom control accepts to handle the -attribute, it calls `defineCustom` to register the custom control with Maya. -Afterward the attributes are added to the list of already handled attributes. -The AE template can then proceed to the next schema. +The constructor of the AE template calls a series of `find`-ing functions to +fit each attribute in a custom control or other special built-in sections. +Afterward it calls `orderSections` to choose the order in which the sections +will be added to the AE template. It then calls `createSchemasSections` to +create the section UI. + +Each `find` function goes through the USD schemas of the USD prim. Some of +the `find` functinare looking for specific schemas. Other are looking for +applied schema or class schemas. A few are not looking at schemas at all +but are grabbing specific kind of attributes. Here are the `find` functions: + +- findAppliedSchemas: find the attributes belonging to applied schemas. +- findClassSchemasL find the attributes belonging to the prim class schemas. +- findSpecialSections: find the shader, transforms and display attributes. + +The `orderSections` function has a list of section it wishes to place first and +a list of sections it wishes to place last. It orders all the sections that were +found according to those lists. All remaining sections that are not forced to be +in a particular order are placed in between. + +The `createSchemasSections` function takes the ordered sections and call the +correct function for each. There is a generic `createSection` to create the UI +for normal sections. There are a few special `create` functions for the special +sections, like shader, material, transforms, metadata, etc. + +The creator function called `createCustomCallbackSection` is special. it allows +sections to be created in a user-supplied callback. See below for more info. + +The `createSection` function checks if the attribute is not supressed and calls +`addControls` to do the UI creation. The `addControls` function goes through each +custom control creator and ask each of them to create the UI for the attributes. +Once a custom control accepts to handle the attribute, it calls `defineCustom` +to register the custom control with Maya. Afterward the attributes are added to +the list of already handled attributes. + ## Custom Controls @@ -109,9 +122,14 @@ There are various helper functions and classes. ## Callbacks -There is a special custom callback section `createCustomCallbackSection()` called during `buildUI()` in the AE template that gives users the opportunity to add layout section(s) to the AE template. For example the Arnold plugin which has its own USD prims. Using this callback section a plugin can organize plugin specific attributes of a prim into AE layout section(s). +There is a special custom callback section `createCustomCallbackSection()` +called during `createSchemasSections()` in the AE template that gives users +the opportunity to add layout sections to the AE template. For example the +Arnold plugin which has its own USD prims. Using this callback section a plugin +can organize plugin specific attributes of a prim into AE layout section(s). -To use this callback section a plugin needs to create a callback function and then register for the AE template UI callback: +To use this callback section a plugin needs to create a callback function and +then register for the AE template UI callback: ```python # AE template UI callback function. @@ -166,7 +184,9 @@ mayaUsdLib.unregisterUICallback('onBuildAETemplate', onBuildAETemplateCallback) ## Attribute Nice Naming callback -A client can add a function to be called when asked for an attribute nice name. That function will be called from `getNiceAttributeName()` giving the client the ability to provide a nice name for the attribute. +A client can add a function to be called when asked for an attribute nice name. +That function will be called from `getNiceAttributeName()` giving the client +the ability to provide a nice name for the attribute. The callback function will receive as input two params: - ufe attribute diff --git a/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py b/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py index 72d022af4f..f6dbf22f6a 100644 --- a/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py +++ b/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py @@ -220,7 +220,7 @@ def __init__(self, ufeSceneItem): # Get the UFE Attributes interface for this scene item. self.attrS = ufe.Attributes.attributes(self.item) - self.addedAttrs = [] + self.addedAttrs = set() self.suppressedAttrs = [] self.hasConnectionObserver = False @@ -233,12 +233,26 @@ def __init__(self, ufeSceneItem): if cmds.optionVar(exists='attrEditorIsLongName'): self.useNiceName = (cmds.optionVar(q='attrEditorIsLongName') ==1) + self.addedMaterialSection = False + + self.suppressArrayAttribute() + + # Build the list of schemas with their associated attributes. + schemasAttributes = { + 'extraAttributes' : [], + 'metadata' : [], + } + + schemasAttributes.update(self.findAppliedSchemas()) + schemasAttributes.update(self.findClassSchemas()) + schemasAttributes.update(self.findSpecialSections()) + + # Order schema sections according to designer's choices. + orderedSchemas = self.orderSections(schemasAttributes) + + # Build the section UI. cmds.editorTemplate(beginScrollLayout=True) - self.buildUI() - self.createAppliedSchemasSection() - self.createCustomCallbackSection() - self.createCustomExtraAttrs() - self.createMetadataSection() + self.createSchemasSections(orderedSchemas, schemasAttributes) cmds.editorTemplate(endScrollLayout=True) if ('%s.%s' % (cmds.about(majorVersion=True), cmds.about(minorVersion=True))) > '2022.1': @@ -258,6 +272,46 @@ def __init__(self, ufeSceneItem): def prependControlCreator(controlCreator): AETemplate._controlCreators.insert(0, controlCreator) + def orderSections(self, schemasAttributes): + ''' + Choose the order in which the sections will be added to the AE template. + ''' + availableSchemas = list(schemasAttributes.keys()) + + desiredFirstSchemas = [ + 'LightAPI', + '.* Light', + 'lightLinkCollectionAPI', + 'shadowLinkCollectionAPI', + 'customCallbacks' + ] + + desiredLastSchemas = [ + 'shader', + 'transforms', + 'display', + 'extraAttributes', + 'metadata', + ] + + def addSchemas(desiredOrder, availableSchemas): + orderedSchemas = [] + for order in desiredOrder: + if '*' in order: + for avail in availableSchemas[:]: + if re.match(order, avail): + availableSchemas.remove(avail) + orderedSchemas.append(avail) + elif order in availableSchemas: + availableSchemas.remove(order) + orderedSchemas.append(order) + return orderedSchemas + + firstSchemas = addSchemas(desiredFirstSchemas, availableSchemas) + lastSchemas = addSchemas(desiredLastSchemas, availableSchemas) + + return firstSchemas + availableSchemas + lastSchemas + def addControls(self, attrNames): for attrName in attrNames: if attrName not in self.suppressedAttrs: @@ -270,7 +324,7 @@ def addControls(self, attrNames): except Exception as ex: # Do not let one custom control failure affect others. print('Failed to create control %s: %s' % (attrName, ex)) - self.addedAttrs.append(attrName) + self.addedAttrs.add(attrName) def suppress(self, control): cmds.editorTemplate(suppress=control) @@ -304,6 +358,7 @@ def sectionNameFromSchema(self, schemaTypeName): ('UsdAbc', ''), ('UsdGeomGprim', 'GeometricPrim'), ('UsdGeomImageable', mel.eval('uiRes(\"m_AEdagNodeTemplate.kDisplay\");')), + ('UsdGeomXformable', getMayaUsdLibString('kTransforms')), ('UsdGeom', ''), ('UsdHydra', ''), ('UsdImagingGL', ''), @@ -335,7 +390,7 @@ def sectionNameFromSchema(self, schemaTypeName): def addShaderLayout(self, group): """recursively create the full attribute layout section""" - with ufeAeTemplate.Layout(self, group.name): + with ufeAeTemplate.Layout(self, group.name, collapse=True): for item in group.items: if isinstance(item, AEShaderLayout.Group): self.addShaderLayout(item) @@ -348,7 +403,7 @@ def isRamp(self): nodeDef = nodeDefHandler.definition(self.item) return nodeDef and nodeDef.type() == "ND_adsk_ramp" - def createShaderAttributesSection(self): + def createShaderAttributesSection(self, sectionName, attrs, collapse): """Use an AEShaderLayout tool to populate the shader section""" # Add a custom control to monitor for connection changed # in order for the UI to update itself when the shader is modified. @@ -374,7 +429,7 @@ def addConnectionObserver(self): obs = UfeConnectionChangedObserver(self.item) self.defineCustom(obs) - def createTransformAttributesSection(self, sectionName, attrsToAdd): + def createTransformAttributesSection(self, sectionName, attrs, collapse): # Get the xformOp order and add those attributes (in order) # followed by the xformOp order attribute. allAttrs = self.attrS.attributeNames @@ -384,8 +439,8 @@ def createTransformAttributesSection(self, sectionName, attrsToAdd): xformOpOrderNames.append(UsdGeom.Tokens.xformOpOrder) # Don't use createSection because we want a sub-sections. - with ufeAeTemplate.Layout(self, sectionName): - attrsToAdd.remove(UsdGeom.Tokens.xformOpOrder) + with ufeAeTemplate.Layout(self, sectionName, collapse=collapse): + attrs.remove(UsdGeom.Tokens.xformOpOrder) self.addControls(xformOpOrderNames) # Get the remainder of the xformOps and add them in an Unused section. @@ -394,29 +449,29 @@ def createTransformAttributesSection(self, sectionName, attrsToAdd): self.createSection(getMayaUsdLibString('kLabelUnusedTransformAttrs'), xformOpUnusedNames, collapse=True) # Then add any reamining Xformable attributes - self.addControls(attrsToAdd) + self.addControls(attrs) # Add a custom control for UFE attribute changed. t3dObs = UfeAttributesObserver(self.item) self.defineCustom(t3dObs) - def createDisplaySection(self, sectionName, attrsToAdd): - with ufeAeTemplate.Layout(self, sectionName, collapse=True): - self.addControls(attrsToAdd) + def createDisplaySection(self, sectionName, attrs, collapse): + with ufeAeTemplate.Layout(self, sectionName, collapse=collapse): + self.addControls(attrs) customDataControl = DisplayCustomControl(self.item, self.prim) usdNoticeControl = UsdNoticeListener(self.prim, [customDataControl]) self.defineCustom(customDataControl) self.defineCustom(usdNoticeControl) - def createMetadataSection(self): + def createMetadataSection(self, sectionName, attrs, collapse): # We don't use createSection() because these are metadata (not attributes). - with ufeAeTemplate.Layout(self, getMayaUsdLibString('kLabelMetadata'), collapse=True): + with ufeAeTemplate.Layout(self, getMayaUsdLibString('kLabelMetadata'), collapse=collapse): metaDataControl = MetadataCustomControl(self.item, self.prim, self.useNiceName) usdNoticeControl = UsdNoticeListener(self.prim, [metaDataControl]) self.defineCustom(metaDataControl) self.defineCustom(usdNoticeControl) - def createCustomExtraAttrs(self): + def createCustomExtraAttrs(self, sectionName, attrs, collapse): # We are not using the maya default "Extra Attributes" section # because we are using custom widget for array type and it's not # possible to inject our widget inside the maya "Extra Attributes" section. @@ -427,12 +482,9 @@ def createCustomExtraAttrs(self): # by addControls(). So any suppressed attributes in extraAttrs will be ignored later. extraAttrs = [attr for attr in self.attrS.attributeNames if attr not in self.addedAttrs] sectionName = mel.eval("uiRes(\"s_TPStemplateStrings.rExtraAttributes\");") - self.createSection(sectionName, extraAttrs, True) - - def createAppliedSchemasSection(self): - usdVer = Usd.GetVersion() - showAppliedSchemasSection = False + self.createSection(sectionName, extraAttrs, collapse) + def findAppliedSchemas(self): # loop on all applied schemas and store all those # schema into a dictionary with the attributes. # Storing the schema into a dictionary allow us to @@ -451,7 +503,8 @@ def createAppliedSchemasSection(self): # "Collection Light Link Include Root" and a comparison with the schema nice name # "Collection Light Link" will allow of to trim the nice name to "Include Root" # - schemaAttrsDict = {} + schemasAttributes = {} + usdVer = Usd.GetVersion() appliedSchemas = self.prim.GetAppliedSchemas() for schema in appliedSchemas: typeAndInstance = Usd.SchemaRegistry().GetTypeNameAndInstance(schema) @@ -473,33 +526,20 @@ def createAppliedSchemasSection(self): prefix = namespace + ":" + instanceName + ":" attrList = [prefix + i for i in attrList] - schemaAttrsDict[instanceName + typeName] = attrList + typeName = instanceName + typeName else: attrList = schemaType.pythonClass.GetSchemaAttributeNames(False) - schemaAttrsDict[typeName] = attrList - - # The "Applied Schemas" will be only visible if at least - # one applied Schemas has attribute. - if not showAppliedSchemasSection: - for attr in attrList: - if self.attrS.hasAttribute(attr): - showAppliedSchemasSection = True - break - # Create the "Applied Schemas" section - # with all the applied schemas - if showAppliedSchemasSection: - with ufeAeTemplate.Layout(self, getMayaUsdLibString('kLabelAppliedSchemas'), collapse=True): - for typeName, attrs in schemaAttrsDict.items(): - typeName = self.sectionNameFromSchema(typeName) - self.createSection(typeName, attrs, False) + schemasAttributes[typeName] = attrList + + return schemasAttributes @staticmethod def getAETemplateForCustomCallback(): global _aeTemplate return _aeTemplate - def createCustomCallbackSection(self): + def createCustomCallbackSection(self, sectionName, attrs, collapse): '''Special custom callback section that gives users the opportunity to add layout section(s) to the AE template. See https://github.com/Autodesk/maya-usd/blob/dev/lib/mayaUsd/resources/ae/usdschemabase/Attribute-Editor-Template-Doc.md @@ -524,26 +564,36 @@ def createCustomCallbackSection(self): print('Failed triggerUICallback: %s' % ex) _aeTemplate = None - def buildUI(self): + def findClassSchemas(self): + schemasAttributes = {} + usdSch = Usd.SchemaRegistry() - self.suppressArrayAttribute() + specialSchemas = { + 'UsdShadeShader', 'UsdShadeNodeGraph', 'UsdShadeMaterial', 'UsdGeomXformable', 'UsdGeomImageable' } - # Track if we already added a connection observer. - self.hasConnectionObserver = False + # We use UFE for the ancestor node types since it caches the + # results by node type. + for schemaType in self.item.ancestorNodeTypes(): + schemaType = usdSch.GetTypeFromName(schemaType) + schemaTypeName = schemaType.typeName + sectionName = self.sectionNameFromSchema(schemaTypeName) + if schemaType.pythonClass: + attrsToAdd = schemaType.pythonClass.GetSchemaAttributeNames(False) + if schemaTypeName in specialSchemas: + continue + schemasAttributes[sectionName] = attrsToAdd + + return schemasAttributes + + def findSpecialSections(self): + schemasAttributes = {} + + usdSch = Usd.SchemaRegistry() # Material has NodeGraph as base. We want to process once for both schema types: hasProcessedMaterial = False - # We want material to be either after the mesh section of the Xformable section, - # whichever comes first, so that it is not too far down in the AE. - self.addedMaterialSection = False - primTypeName = self.sectionNameFromSchema(self.prim.GetTypeName()) - def addMatSection(): - if not self.addedMaterialSection: - self.addedMaterialSection = True - self.createMaterialAttributeSection() - # We use UFE for the ancestor node types since it caches the # results by node type. for schemaType in self.item.ancestorNodeTypes(): @@ -557,23 +607,61 @@ def addMatSection(): if hasProcessedMaterial: continue # Shader attributes are special - self.createShaderAttributesSection() + schemasAttributes['shader'] = [] hasProcessedMaterial = True # Note: don't show the material section for materials. self.addedMaterialSection = True # We have a special case when building the Xformable section. elif schemaTypeName == 'UsdGeomXformable': - self.createTransformAttributesSection(sectionName, attrsToAdd) + schemasAttributes['transforms'] = attrsToAdd elif schemaTypeName == 'UsdGeomImageable': - self.createDisplaySection(sectionName, attrsToAdd) - else: - sectionsToCollapse = ['Curves', 'Point Based', 'Geometric Prim', 'Boundable', - 'Imageable', 'Field Asset', 'Light'] - collapse = sectionName in sectionsToCollapse - self.createSection(sectionName, attrsToAdd, collapse) + schemasAttributes['display'] = attrsToAdd + + return schemasAttributes + + def createSchemasSections(self, schemasOrder, schemasAttributes): + # We want material to be either after the mesh section of the Xformable section, + # whichever comes first, so that it is not too far down in the AE. + primTypeName = self.sectionNameFromSchema(self.prim.GetTypeName()) + def addMatSection(): + if not self.addedMaterialSection: + self.addedMaterialSection = True + self.createMaterialAttributeSection() - if sectionName == primTypeName: - addMatSection() + # Function that determines if a section should be expanded. + def isSectionOpen(sectionName): + if sectionName == primTypeName: + return True + if sectionName == 'material': + return True + lowerName = sectionName.lower() + return 'light' in lowerName and 'link' not in lowerName + + # Dictionry of which function to call to create a given section. + # By default, calls the generic createSection, which will search + # in the list of known custom control creators for the one to be + # used. + sectionCreators = collections.defaultdict( + lambda : self.createSection, + { + 'shader': self.createShaderAttributesSection, + 'transforms': self.createTransformAttributesSection, + 'display': self.createDisplaySection, + 'extraAttributes': self.createCustomExtraAttrs, + 'metadata': self.createMetadataSection, + 'customCallbacks': self.createCustomCallbackSection, + }) + + # Create the section in the specified order. + for typeName in schemasOrder: + attrs = schemasAttributes[typeName] + sectionName = self.sectionNameFromSchema(typeName) + collapse = not isSectionOpen(sectionName) + creator = sectionCreators[typeName] + creator(sectionName, attrs, collapse) + + if sectionName == primTypeName: + addMatSection() # In case there was neither a Mesh nor Xformable section, add material section now. addMatSection() @@ -586,8 +674,7 @@ def createMaterialAttributeSection(self): if not mat: return layoutName = getMayaUsdLibString('kLabelMaterial') - collapse = False - with ufeAeTemplate.Layout(self, layoutName, collapse): + with ufeAeTemplate.Layout(self, layoutName, collapse=False): createdControl = MaterialCustomControl(self.item, self.prim, self.useNiceName) self.defineCustom(createdControl) diff --git a/lib/mayaUsd/resources/scripts/mayaUsdLibRegisterStrings.py b/lib/mayaUsd/resources/scripts/mayaUsdLibRegisterStrings.py index 322e79a8fd..74fc9528d9 100644 --- a/lib/mayaUsd/resources/scripts/mayaUsdLibRegisterStrings.py +++ b/lib/mayaUsd/resources/scripts/mayaUsdLibRegisterStrings.py @@ -40,7 +40,6 @@ def mayaUsdLibUnregisterStrings(): 'kMenuPrintValue': 'Print to Script Editor', 'kLabelUnusedTransformAttrs': 'Unused', 'kLabelMetadata': 'Metadata', - 'kLabelAppliedSchemas': 'Applied Schemas', 'kOpenImage': 'Open', 'kLabelMaterial': 'Material', 'kLabelAssignedMaterial': 'Assigned Material', @@ -57,6 +56,7 @@ def mayaUsdLibUnregisterStrings(): 'kLabelMaterialNewTab': 'New Tab...', 'kUseOutlinerColorAnn': 'Apply the Outliner color to the display of the prim name in the Outliner.', 'kOutlinerColorAnn': 'The color of the text displayed in the Outliner.', + 'kTransforms': 'Transforms', # mayaUsdAddMayaReference.py 'kErrorGroupPrimExists': 'Group prim "^1s" already exists under "^2s". Choose prim name other than "^1s" to proceed.', diff --git a/test/lib/testAttributeEditorTemplate.py b/test/lib/testAttributeEditorTemplate.py index 9dfa72df90..78f3a62055 100644 --- a/test/lib/testAttributeEditorTemplate.py +++ b/test/lib/testAttributeEditorTemplate.py @@ -19,16 +19,15 @@ import unittest import mayaUtils -import ufeUtils import fixturesUtils import testUtils -import mayaUsd.lib - from maya import cmds +from maya import mel from maya import standalone from mayaUsdLibRegisterStrings import getMayaUsdLibString +import mayaUsd_createStageWithNewLayer from pxr import Usd @@ -66,6 +65,68 @@ def searchForMayaControl(self, controlOrLayout, mayaControlCmd, labelToFind): if mayaControlCmd(child, q=True, label=True) == labelToFind: return child return None + + def findAllFrameLayoutLabels(self, startLayout): + ''' + Find all child frame layout and return a list of their labels. + ''' + found = [] + + if not startLayout: + return found + + childrenOfLayout = cmds.layout(startLayout, q=True, ca=True) + if not childrenOfLayout: + return found + + for child in childrenOfLayout: + child = startLayout + '|' + child + if cmds.frameLayout(child, exists=True): + found.append(cmds.frameLayout(child, q=True, label=True)) + elif cmds.layout(child, exists=True): + found += self.findAllFrameLayoutLabels(child) + + return found + + def findExpandedFrameLayout(self, startLayout, labelToFind, primPath): + ''' + Find and expand the given frame layout and return the new updated layout. + We unfortunately need to explicitly update the AE for this to work. + ''' + frameLayout = self.searchForMayaControl(startLayout, cmds.frameLayout, labelToFind) + self.assertIsNotNone(frameLayout, 'Could not find "%s" frameLayout' % labelToFind) + self.expandFrameLayout(frameLayout, primPath) + return self.searchForMayaControl(startLayout, cmds.frameLayout, labelToFind) + + def expandFrameLayout(self, frameLayout, primPath): + ''' + Expand the given frame layout and refresh the AE. + You will need to find the layout again after that since it might have changed name. + ''' + cmds.frameLayout(frameLayout, edit=True, collapse=False) + mel.eval('''updateAE "%s"''' % primPath) + + def findAllNonLayout(self, startLayout): + ''' + Find all child non-layout controls and return them as a list. + ''' + found = [] + + if not startLayout: + return found + + childrenOfLayout = cmds.layout(startLayout, q=True, ca=True) + if not childrenOfLayout: + return found + + for child in childrenOfLayout: + child = startLayout + '|' + child + if cmds.layout(child, exists=True): + found += self.findAllNonLayout(child) + else: + found.append(child) + + return found def attrEdFormLayoutName(self, obj): # Ufe runtime name (USD) was added to formLayout name in 2022.2. @@ -81,7 +142,6 @@ def testAETemplate(self): # Create a simple USD scene with a single prim. cmds.file(new=True, force=True) - import mayaUsd_createStageWithNewLayer proxyShape = mayaUsd_createStageWithNewLayer.createStageWithNewLayer() proxyShapePath = ufe.PathString.path(proxyShape) proxyShapeItem = ufe.Hierarchy.createItem(proxyShapePath) @@ -90,7 +150,8 @@ def testAETemplate(self): # select the prim from 'Add New Prim', so always select it here. proxyShapeContextOps = ufe.ContextOps.contextOps(proxyShapeItem) proxyShapeContextOps.doOp(['Add New Prim', 'Capsule']) - cmds.select(proxyShape + ",/Capsule1") + fullPrimPath = proxyShape + ",/Capsule1" + cmds.select(fullPrimPath) # Enable the display of Array Attributes. cmds.optionVar(intValue=('mayaUSD_AEShowArrayAttributes', 1)) @@ -112,7 +173,7 @@ def testAETemplate(self): # We should have a frameLayout called 'Capsule' in the template. # If there is a scripting error in the template, this layout will be missing. - frameLayout = self.searchForMayaControl(startLayout, cmds.frameLayout, 'Capsule') + frameLayout = self.findExpandedFrameLayout(startLayout, 'Capsule', fullPrimPath) self.assertIsNotNone(frameLayout, 'Could not find Capsule frameLayout') # We should also have float slider controls for 'Height' & 'Radius'. @@ -134,7 +195,7 @@ def testAETemplate(self): # We should have a frameLayout called 'Metadata' in the template. # If there is a scripting error in the template, this layout will be missing. - frameLayout = self.searchForMayaControl(startLayout, cmds.frameLayout, getMayaUsdLibString('kLabelMetadata')) + frameLayout = self.findExpandedFrameLayout(startLayout, getMayaUsdLibString('kLabelMetadata'), fullPrimPath) self.assertIsNotNone(frameLayout, 'Could not find Metadata frameLayout') # Create a SphereLight via contextOps menu. @@ -150,16 +211,6 @@ def testAETemplate(self): startLayout = cmds.formLayout(lightFormLayout, query=True, fullPathName=True) self.assertIsNotNone(startLayout, 'Could not get full path for SphereLight formLayout') - # -------------------------------------------------------------------------------- - # Test the 'createAppliedSchemasSection' method of template. - # Requires USD 0.21.2 - # -------------------------------------------------------------------------------- - if Usd.GetVersion() >= (0, 21, 2): - # We should have a frameLayout called 'Applied Schemas' in the template. - # If there is a scripting error in the template, this layout will be missing. - frameLayout = self.searchForMayaControl(startLayout, cmds.frameLayout, getMayaUsdLibString('kLabelAppliedSchemas')) - self.assertIsNotNone(frameLayout, 'Could not find Applied Schemas frameLayout') - # -------------------------------------------------------------------------------- # Test the 'createCustomExtraAttrs' method of template. # -------------------------------------------------------------------------------- @@ -187,7 +238,6 @@ def testCreateDisplaySection(self): # Create a simple USD scene with a single prim. cmds.file(new=True, force=True) - import mayaUsd_createStageWithNewLayer proxyShape = mayaUsd_createStageWithNewLayer.createStageWithNewLayer() proxyShapePath = ufe.PathString.path(proxyShape) proxyShapeItem = ufe.Hierarchy.createItem(proxyShapePath) @@ -196,7 +246,8 @@ def testCreateDisplaySection(self): # select the prim from 'Add New Prim', so always select it here. proxyShapeContextOps = ufe.ContextOps.contextOps(proxyShapeItem) proxyShapeContextOps.doOp(['Add New Prim', 'Capsule']) - cmds.select(proxyShape + ",/Capsule1") + fullPrimPath = proxyShape + ",/Capsule1" + cmds.select(fullPrimPath) # Make sure the AE is visible. import maya.mel @@ -206,7 +257,8 @@ def findDisplayLayout(): capsuleFormLayout = self.attrEdFormLayoutName('Capsule') startLayout = cmds.formLayout(capsuleFormLayout, query=True, fullPathName=True) kDisplay = maya.mel.eval('uiRes(\"m_AEdagNodeTemplate.kDisplay\");') - return self.searchForMayaControl(startLayout, cmds.frameLayout, kDisplay) + return self.findExpandedFrameLayout(startLayout, kDisplay, fullPrimPath) + # -------------------------------------------------------------------------------- # Test the 'createDisplaySection' method of template. @@ -217,15 +269,6 @@ def findDisplayLayout(): # Maya 2022 doesn't remember and restore the expand/collapse state, # so skip this part of the test. if mayaUtils.mayaMajorVersion() > 2022: - # Expand the Display layout then update the AE again. - # We must do this because when we expand it doesn't happen right away. - # But by expanding it when we updateAE the capsule the display section - # expansion is remembered and auto-expanded. - cmds.frameLayout(displayLayout, edit=True, collapse=False) - maya.mel.eval('updateAE "|stage1|stageShape1,/Capsule1"') - displayLayout = findDisplayLayout() - self.assertIsNotNone(displayLayout, 'Could not find Display frameLayout') - # We should have 'Visibility' optionMenuGrp (which is a label (text) and optionMenu). visControl = self.searchForMayaControl(displayLayout, cmds.text, 'Visibility') self.assertIsNotNone(visControl) @@ -242,7 +285,8 @@ def testAECustomMaterialControl(self): shapeNode,shapeStage = mayaUtils.createProxyFromFile(testFile) # Select this prim which has a custom material control attribute. - cmds.select('|stage|stageShape,/pCube1', r=True) + fullPrimPath = shapeNode + ',/pCube1' + cmds.select(fullPrimPath, r=True) # Make sure the AE is visible. import maya.mel @@ -257,7 +301,7 @@ def testAECustomMaterialControl(self): # In the AE there is a formLayout for each USD prim type. We start # by making sure we can find the one for the mesh. - materialFormLayout = self.searchForMayaControl(startLayout, cmds.frameLayout, 'Material') + materialFormLayout = self.findExpandedFrameLayout(startLayout, 'Material', fullPrimPath) self.assertIsNotNone(materialFormLayout, 'Could not find "Material" frameLayout') # We can now search for the control for the meterial. @@ -273,7 +317,8 @@ def testAECustomImageControl(self): shapeNode,shapeStage = mayaUtils.createProxyFromFile(testFile) # Select this prim which has a custom image control attribute. - cmds.select('|stage|stageShape,/TypeSampler/MaterialX/D_filename', r=True) + fullPrimPath = shapeNode + ',/TypeSampler/MaterialX/D_filename' + cmds.select(fullPrimPath, r=True) # Make sure the AE is visible. import maya.mel @@ -289,7 +334,7 @@ def testAECustomImageControl(self): if mayaUtils.mayaMajorVersion() > 2024: # We should have a frameLayout called 'Shader: Dot' in the template. # If there is a scripting error in the template, this layout will be missing. - frameLayout = self.searchForMayaControl(startLayout, cmds.frameLayout, 'Shader: Dot') + frameLayout = self.findExpandedFrameLayout(startLayout, 'Shader: Dot', fullPrimPath) self.assertIsNotNone(frameLayout, 'Could not find "Shader: Dot" frameLayout') # We should also have custom image control for 'In'. @@ -309,7 +354,8 @@ def testAECustomEnumControl(self): shapeNode,shapeStage = mayaUtils.createProxyFromFile(testFile) # Select this prim which has a custom image control attribute. - cmds.select('|stage|stageShape,/Material1/gltf_pbr1', r=True) + fullPrimPath = shapeNode + ',/Material1/gltf_pbr1' + cmds.select(fullPrimPath, r=True) # Make sure the AE is visible. import maya.mel @@ -321,12 +367,35 @@ def testAECustomEnumControl(self): self.assertTrue(cmds.formLayout(shaderFormLayout, exists=True)) startLayout = cmds.formLayout(shaderFormLayout, query=True, fullPathName=True) self.assertIsNotNone(startLayout, 'Could not get full path for Shader formLayout') - + + frameLayout = self.searchForMayaControl(startLayout, cmds.frameLayout, 'Shader: glTF PBR') + self.assertIsNotNone(frameLayout, 'Could not find "Shader: glTF PBR" frameLayout') + + self.expandFrameLayout(frameLayout, fullPrimPath) + + shaderFormLayout = self.attrEdFormLayoutName('Shader') + self.assertTrue(cmds.formLayout(shaderFormLayout, exists=True)) + startLayout = cmds.formLayout(shaderFormLayout, query=True, fullPathName=True) + self.assertIsNotNone(startLayout, 'Could not get full path for Shader formLayout') + frameLayout = self.searchForMayaControl(startLayout, cmds.frameLayout, 'Shader: glTF PBR') + self.assertIsNotNone(frameLayout, 'Could not find "Shader: glTF PBR" frameLayout') + # We should have a frameLayout called 'Alpha' in the template. # If there is a scripting error in the template, this layout will be missing. frameLayout = self.searchForMayaControl(startLayout, cmds.frameLayout, 'Alpha') self.assertIsNotNone(frameLayout, 'Could not find "Alpha" frameLayout') - + + self.expandFrameLayout(frameLayout, fullPrimPath) + + shaderFormLayout = self.attrEdFormLayoutName('Shader') + self.assertTrue(cmds.formLayout(shaderFormLayout, exists=True)) + startLayout = cmds.formLayout(shaderFormLayout, query=True, fullPathName=True) + self.assertIsNotNone(startLayout, 'Could not get full path for Shader formLayout') + frameLayout = self.searchForMayaControl(startLayout, cmds.frameLayout, 'Shader: glTF PBR') + self.assertIsNotNone(frameLayout, 'Could not find "Shader: glTF PBR" frameLayout') + frameLayout = self.searchForMayaControl(startLayout, cmds.frameLayout, 'Alpha') + self.assertIsNotNone(frameLayout, 'Could not find "Alpha" frameLayout') + # We should also have custom enum control for 'Inputs Alpha Mode'. InputsAlphaModeControl = self.searchForMayaControl(frameLayout, cmds.text, 'Alpha Mode') self.assertIsNotNone(InputsAlphaModeControl, 'Could not find gltf_pbr1 "Alpha Mode" control') @@ -339,7 +408,8 @@ def testAEConnectionsCustomControl(self): testPath,shapeStage = mayaUtils.createProxyFromFile(testFile) # Select this prim which has an attribute with a connection. - cmds.select('|stage|stageShape,/Material1/fractal3d1', r=True) + fullPrimPath = testPath + ',/Material1/fractal3d1' + cmds.select(fullPrimPath, r=True) # Make sure the AE is visible. import maya.mel @@ -355,7 +425,7 @@ def testAEConnectionsCustomControl(self): if mayaUtils.mayaMajorVersion() > 2024: # We should have a frameLayout called 'Shader: 3D Fractal Noise' in the template. # If there is a scripting error in the template, this layout will be missing. - frameLayout = self.searchForMayaControl(startLayout, cmds.frameLayout, 'Shader: 3D Fractal Noise') + frameLayout = self.findExpandedFrameLayout(startLayout, 'Shader: 3D Fractal Noise', fullPrimPath) self.assertIsNotNone(frameLayout, 'Could not find "Shader: 3D Fractal Noise" frameLayout') # We should also have an attribute called 'Amplitude' which has a connection. @@ -369,7 +439,8 @@ def testAEConnectionsCustomControlWithComponents(self): testPath,shapeStage = mayaUtils.createProxyFromFile(testFile) # Select this prim which has an attribute with a connection. - cmds.select('|stage|stageShape,/Material1/component_add', r=True) + fullPrimPath = testPath + ',/Material1/component_add' + cmds.select(fullPrimPath, r=True) # Make sure the AE is visible. import maya.mel @@ -385,7 +456,7 @@ def testAEConnectionsCustomControlWithComponents(self): if mayaUtils.mayaMajorVersion() > 2024: # We should have a frameLayout called 'Shader: Add' in the template. # If there is a scripting error in the template, this layout will be missing. - frameLayout = self.searchForMayaControl(startLayout, cmds.frameLayout, 'Shader: Add') + frameLayout = self.findExpandedFrameLayout(startLayout, 'Shader: Add', fullPrimPath) self.assertIsNotNone(frameLayout, 'Could not find "Shader: Add" frameLayout') # We should also have an attribute called 'In1' which has component connections. @@ -400,6 +471,62 @@ def testAEConnectionsCustomControlWithComponents(self): for menuItem in cmds.popupMenu(popupMenu, q=True, itemArray=True): self.assertIn(cmds.menuItem(popupMenu + "|" + menuItem, q=True, l=True), expectedLabels) + def testAEForLight(self): + '''Test that the expected sections are created for lights.''' + proxyShape = mayaUsd_createStageWithNewLayer.createStageWithNewLayer() + proxyShapePath = ufe.PathString.path(proxyShape) + proxyShapeItem = ufe.Hierarchy.createItem(proxyShapePath) + + # Create a cylinder light via contextOps menu. Not all versions of Maya automatically + # select the prim from 'Add New Prim', so always select it here. + proxyShapeContextOps = ufe.ContextOps.contextOps(proxyShapeItem) + proxyShapeContextOps.doOp(['Add New Prim', 'All Registered', 'Lighting', 'CylinderLight']) + primName = 'CylinderLight1' + fullPrimPath = proxyShape + ',/%s' % primName + cmds.select(fullPrimPath) + + # Make sure the AE is visible. + import maya.mel + maya.mel.eval('openAEWindow') + + primFormLayout = self.attrEdFormLayoutName('CylinderLight') + self.assertTrue(cmds.formLayout(primFormLayout, exists=True), 'Layout for %s was not found\n' % primName) + + startLayout = cmds.formLayout(primFormLayout, query=True, fullPathName=True) + self.assertIsNotNone(startLayout, 'Could not get full path for %s formLayout' % primName) + + # Augment the maixmum diff size to get better error message when comparing the lists. + self.maxDiff = 2000 + + actualSectionLabels = self.findAllFrameLayoutLabels(startLayout) + + # Note: different version of USD can have different schemas, + # so we only compare the ones we are interested in verifying. + expectedInitialSectionLabels = [ + 'Light ', + 'Cylinder Light', + 'Light Link Collection ', + 'Shadow Link Collection '] + self.assertListEqual( + actualSectionLabels[0:len(expectedInitialSectionLabels)], + expectedInitialSectionLabels) + + # Note: there are no extra attributes in Maya 2022. + if mayaUtils.mayaMajorMinorVersions() >= (2023, 0): + expectedFinalSectionLabels = [ + 'Transforms', + 'Display', + 'Extra Attributes', + 'Metadata'] + else: + expectedFinalSectionLabels = [ + 'Transforms', + 'Display', + 'Metadata'] + self.assertListEqual( + actualSectionLabels[-len(expectedFinalSectionLabels):], + expectedFinalSectionLabels) + if __name__ == '__main__': fixturesUtils.runTests(globals()) From fe2f9fbcc85ef8671d5a1eb1191acde2c1ac5e91 Mon Sep 17 00:00:00 2001 From: "Pierre B." <78159064+pierrebai-adsk@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:26:21 -0400 Subject: [PATCH 2/3] Update lib/mayaUsd/resources/ae/usdschemabase/ae_template.py Co-authored-by: Sean Donnelly <23455376+seando-adsk@users.noreply.github.com> --- lib/mayaUsd/resources/ae/usdschemabase/ae_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py b/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py index f6dbf22f6a..30fc1dedaf 100644 --- a/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py +++ b/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py @@ -637,7 +637,7 @@ def isSectionOpen(sectionName): lowerName = sectionName.lower() return 'light' in lowerName and 'link' not in lowerName - # Dictionry of which function to call to create a given section. + # Dictionary of which function to call to create a given section. # By default, calls the generic createSection, which will search # in the list of known custom control creators for the one to be # used. From a2091cd3f41e54586bb8130f73620487aec0cfd3 Mon Sep 17 00:00:00 2001 From: "Pierre B." <78159064+pierrebai-adsk@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:26:46 -0400 Subject: [PATCH 3/3] Update lib/mayaUsd/resources/ae/usdschemabase/Attribute-Editor-Template-Doc.md Co-authored-by: Sean Donnelly <23455376+seando-adsk@users.noreply.github.com> --- .../resources/ae/usdschemabase/Attribute-Editor-Template-Doc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mayaUsd/resources/ae/usdschemabase/Attribute-Editor-Template-Doc.md b/lib/mayaUsd/resources/ae/usdschemabase/Attribute-Editor-Template-Doc.md index 0dfa1a29aa..0e54035ed1 100644 --- a/lib/mayaUsd/resources/ae/usdschemabase/Attribute-Editor-Template-Doc.md +++ b/lib/mayaUsd/resources/ae/usdschemabase/Attribute-Editor-Template-Doc.md @@ -34,7 +34,7 @@ applied schema or class schemas. A few are not looking at schemas at all but are grabbing specific kind of attributes. Here are the `find` functions: - findAppliedSchemas: find the attributes belonging to applied schemas. -- findClassSchemasL find the attributes belonging to the prim class schemas. +- findClassSchemas: find the attributes belonging to the prim class schemas. - findSpecialSections: find the shader, transforms and display attributes. The `orderSections` function has a list of section it wishes to place first and