From dcab7f303b3586eb62e0b9c0702f9563309e6ac5 Mon Sep 17 00:00:00 2001 From: Jerry Gamache Date: Thu, 12 Jan 2023 10:48:18 -0500 Subject: [PATCH 1/2] Shorten nice names by trimming schema name Some attribute names use namespaces that are copies of the schema name. When displaying UI names we want to strip that redundant information. --- .../resources/ae/usdschemabase/ae_template.py | 27 +-- lib/mayaUsd/ufe/UsdAttributeHolder.cpp | 177 +++++++++++++++++- test/lib/ufe/testAttribute.py | 42 ++++- 3 files changed, 230 insertions(+), 16 deletions(-) diff --git a/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py b/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py index 1373949d77..6310f8f0a3 100644 --- a/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py +++ b/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py @@ -694,11 +694,9 @@ def sectionNameFromSchema(self, schemaTypeName): schemaTypeName = mayaUsdLib.Util.prettifyName(schemaTypeName) - # if the schema name ends with "api", replace it with - # "API" and make sure to only replace the last occurence. - # Example: "Shaping api" which contains "api" twice most become "Shaping API" - if schemaTypeName.endswith("api"): - schemaTypeName = " API".join(schemaTypeName.rsplit("api", 1)) + # if the schema name ends with "api" or "API", trim it. + if schemaTypeName.endswith("api") or schemaTypeName.endswith("API"): + schemaTypeName = schemaTypeName[:-3] return schemaTypeName @@ -790,9 +788,17 @@ def createAppliedSchemasSection(self): # group all instances of a MultipleApply schema together # so we can later display them into the same UI section. # - # By example, if UsdCollectionAPI is applied twice, UsdPrim.GetAppliedSchemas() - # will return ["CollectionAPI:instance1","CollectionAPI:instance2"] but we want to group - # both instance inside a "CollectionAPI" section. + # We do not group the collections API. We rather create a section that + # includes the instance in the title. This allows up to optimally trim + # the type name and the namespace when generating the attribute nice names. + # + # By example, on a UsdLux light, we have two UsdCollectionAPI applied. + # UsdPrim.GetAppliedSchemas() will return ["CollectionAPI:lightLink", + # "CollectionAPI:shadowLink"] and we will create two sections named + # "Light Link Collection" and "Shadow Link Collection". An attribute + # named "collection:lightLink:includeRoot" will get the base nice name + # "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 = {} appliedSchemas = self.prim.GetAppliedSchemas() @@ -819,10 +825,7 @@ def createAppliedSchemasSection(self): prefix = namespace + ":" + instanceName + ":" attrList = [prefix + i for i in attrList] - if typeName in schemaAttrsDict: - schemaAttrsDict[typeName] += attrList - else: - schemaAttrsDict[typeName] = attrList + schemaAttrsDict[instanceName + typeName] = attrList else: attrList = schemaType.pythonClass.GetSchemaAttributeNames(False) schemaAttrsDict[typeName] = attrList diff --git a/lib/mayaUsd/ufe/UsdAttributeHolder.cpp b/lib/mayaUsd/ufe/UsdAttributeHolder.cpp index 0e5107235e..757b09c96b 100644 --- a/lib/mayaUsd/ufe/UsdAttributeHolder.cpp +++ b/lib/mayaUsd/ufe/UsdAttributeHolder.cpp @@ -206,16 +206,187 @@ Ufe::Value UsdAttributeHolder::getMetadata(const std::string& key) const } } if (key == MayaUsdMetadata->UIName) { + std::string niceName; // Non-shader case, but we still have light inputs and outputs to deal with: if (PXR_NS::UsdShadeInput::IsInput(_usdAttr)) { const PXR_NS::UsdShadeInput input(_usdAttr); - return Ufe::Value(UsdMayaUtil::prettifyName(input.GetBaseName().GetString())); + niceName = input.GetBaseName().GetString(); } else if (PXR_NS::UsdShadeOutput::IsOutput(_usdAttr)) { const PXR_NS::UsdShadeOutput output(_usdAttr); - return Ufe::Value(UsdMayaUtil::prettifyName(output.GetBaseName().GetString())); + niceName = output.GetBaseName().GetString(); } else { - return Ufe::Value(UsdMayaUtil::prettifyName(_usdAttr.GetName().GetString())); + niceName = _usdAttr.GetName().GetString(); } + + const bool isNamespaced = niceName.find(":") != std::string::npos; + niceName = UsdMayaUtil::prettifyName(niceName); + + if (!isNamespaced) { + return Ufe::Value(niceName); + } + +#if PXR_VERSION > 2203 + // We can further prettify the nice name by removing all prefix that are an exact copy + // of the applied schema name. + // + // For example an attribute named ui:nodegraph:node:pos found in UsdUINodeGraphNodeAPI + // can be further simplified to "Pos". + using DefVec = std::vector>; + DefVec defsToExplore; + + const UsdSchemaRegistry& schemaReg = UsdSchemaRegistry::GetInstance(); + for (auto&& name : _usdAttr.GetPrim().GetAppliedSchemas()) { + + std::pair typeNameAndInstance + = schemaReg.GetTypeNameAndInstance(name); + + const UsdPrimDefinition* primDef + = schemaReg.FindAppliedAPIPrimDefinition(typeNameAndInstance.first); + if (!primDef) { + primDef = schemaReg.FindConcretePrimDefinition(typeNameAndInstance.first); + } + if (!primDef) { + continue; + } + defsToExplore.emplace_back(name, primDef); + } + + // Sort the schemas by number of applied schemas to make sure we associate the attribute + // to the smallest schema that defines it. + std::sort( + defsToExplore.begin(), + defsToExplore.end(), + [](const DefVec::value_type& a, const DefVec::value_type& b) { + return a.second->GetAppliedAPISchemas().size() + < b.second->GetAppliedAPISchemas().size(); + }); + + for (auto&& nameAndPrimDef : defsToExplore) { + std::pair typeNameAndInstance + = schemaReg.GetTypeNameAndInstance(nameAndPrimDef.first); + if (typeNameAndInstance.second.IsEmpty()) { + const auto& names = nameAndPrimDef.second->GetPropertyNames(); + if (std::find(names.cbegin(), names.cend(), _usdAttr.GetName()) + == names.cend()) { + continue; + } + } else { + // Multi-apply schema. There is a nice gymnastic to do if we want to prove the + // attribute belongs to that schema. + const auto& names = nameAndPrimDef.second->GetPropertyNames(); + if (names.empty()) { + continue; + } + + // Get the template from the first attribute name to build the instance prefix. + // USD currently uses __INSTANCE_NAME__, but there is no way to programmatically + // get that string. Look for a double underscore instead. + std::string prefix = names.front(); + size_t dunderPos = prefix.find("__"); + if (dunderPos == std::string::npos) { + continue; + } + + prefix = prefix.substr(0, dunderPos) + typeNameAndInstance.second.GetString() + + ":"; + + // If the parameter name does not start with the templte, it does not belong to + // this API: + if (_usdAttr.GetName().GetString().rfind(prefix, 0) == std::string::npos) { + continue; + } + } + + // Now we want to strip any token sequence found in the schema API name. + // + // A few examples: + // + // Namespaced name: | API name | Nice name: + // ----------------------------------+--------------------------+----------- + // shaping:cone:angle | ShapingAPI | Cone Angle + // ui:nodegraph:node:pos | NodeGraphNodeAPI | Ui Pos + // collections:lightLink:includeRoot | CollectionAPI(LightLink) | Include Root + // + // You can already note two issues with NodeGraph. First the namespace begins with + // "ui", and second, "nodegraph" is not camelCased which means it will prettify + // as a single token instead of two. We somehow need to include these observations + // when pruning schema name tokens. + + // If the schema name ends with API, trim that. + std::string schemaName = typeNameAndInstance.first; + if (schemaName.find("API", schemaName.size() - 3) != std::string::npos) { + schemaName = schemaName.substr(0, schemaName.size() - 3); + } + + // Add the instance name for multi-apply schemas: + if (!typeNameAndInstance.second.IsEmpty()) { + schemaName += typeNameAndInstance.second.GetString(); + } + + // Lowercase everything (because "nodegraph"): + auto toLower = [](std::string& s) { + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { + return std::tolower(c); + }); + }; + toLower(schemaName); + + // Split the attribute nice names into tokens and see if we find a sequence of + // tokens that match the lowercased schema name. + auto attributeTokens = PXR_NS::TfStringSplit(niceName, " "); + + // Rebuild nice name using the tokens: + niceName = ""; + auto tokenIt = attributeTokens.begin(); + while (tokenIt != attributeTokens.end()) { + size_t substringSize = tokenIt->size(); + auto lastIt = tokenIt + 1; + while (substringSize < schemaName.size() && lastIt != attributeTokens.end()) { + substringSize += lastIt->size(); + ++lastIt; + } + + // We have enough information to build a substring. + if (substringSize == schemaName.size()) { + std::string substring; + for (auto it = tokenIt; it != lastIt; ++it) { + substring += *it; + } + toLower(substring); + if (substring == schemaName) { + // Exact match. We can skip these tokens: + tokenIt = lastIt; + break; + } + } + + // No match here. Add the current token to the nice name: + if (niceName.empty()) { + niceName = *tokenIt; + } else { + niceName += " " + *tokenIt; + } + + // Get ready for next iteration: + ++tokenIt; + + // We can exit the loop if we have run out of tokens and can not build a + // substring of sufficient size. + if (lastIt == attributeTokens.end() && substringSize < schemaName.size()) { + break; + } + } + // Add whatever token remains to the nice name: + for (; tokenIt != attributeTokens.end(); ++tokenIt) { + if (niceName.empty()) { + niceName = *tokenIt; + } else { + niceName += " " + *tokenIt; + } + } + } +#endif + return Ufe::Value(niceName); } PXR_NS::VtValue v; if (_usdAttr.GetMetadata(tok, &v)) { diff --git a/test/lib/ufe/testAttribute.py b/test/lib/ufe/testAttribute.py index d127d5755f..1a2e4658f5 100644 --- a/test/lib/ufe/testAttribute.py +++ b/test/lib/ufe/testAttribute.py @@ -22,7 +22,7 @@ import testUtils import usdUtils -from pxr import Usd, UsdGeom, Vt, Gf +from pxr import Usd, UsdGeom, Vt, Gf, UsdLux, UsdUI from pxr import UsdShade from maya import cmds @@ -1963,5 +1963,45 @@ def testAttributeMetadataChanged(self): self.assertEqual(str(attr.getMetadata(uisoftminKey)), str(value)) + @unittest.skipUnless(Usd.GetVersion() >= (0, 22, 8), 'Requires a recent UsdLux API') + def testAttributeNiceNames(self): + cmds.file(new=True, force=True) + + # Create a proxy shape with empty stage to start with. + import mayaUsd_createStageWithNewLayer + proxyShape = mayaUsd_createStageWithNewLayer.createStageWithNewLayer() + + # Create a ContextOps interface for the proxy shape. + proxyShapePath = ufe.Path([mayaUtils.createUfePathSegment(proxyShape)]) + proxyShapeItem = ufe.Hierarchy.createItem(proxyShapePath) + contextOps = ufe.ContextOps.contextOps(proxyShapeItem) + + # Add a sphere light prim: + ufeCmd.execute(contextOps.doOpCmd(['Add New Prim', 'SphereLight'])) + + proxyShapehier = ufe.Hierarchy.hierarchy(proxyShapeItem) + lightItem = proxyShapehier.children()[0] + lightPrim = usdUtils.getPrimFromSceneItem(lightItem) + UsdLux.ShadowAPI.Apply(lightPrim) + UsdLux.ShapingAPI.Apply(lightPrim) + UsdUI.NodeGraphNodeAPI.Apply(lightPrim) + + ufeAttrs = ufe.Attributes.attributes(lightItem) + + testCases = ( + ("inputs:radius", "Radius"), + ("inputs:shadow:falloffGamma", "Falloff Gamma"), + ("collection:shadowLink:expansionRule", "Expansion Rule"), + ("collection:lightLink:expansionRule", "Expansion Rule"), + ("inputs:shaping:cone:angle", "Cone Angle"), + ("ui:nodegraph:node:pos", "Ui Pos") + ) + + for attrName, niceName in testCases: + attr = ufeAttrs.attribute(attrName) + self.assertIsNotNone(attr) + self.assertEqual(attr.getMetadata("uiname"), niceName) + + if __name__ == '__main__': unittest.main(verbosity=2) From 5a9bea70cb0a64928d4b45c1d7fc350f0944b718 Mon Sep 17 00:00:00 2001 From: Jerry Gamache Date: Thu, 12 Jan 2023 12:32:01 -0500 Subject: [PATCH 2/2] Fix typo --- lib/mayaUsd/resources/ae/usdschemabase/ae_template.py | 2 +- lib/mayaUsd/ufe/UsdAttributeHolder.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py b/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py index 6310f8f0a3..2798916506 100644 --- a/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py +++ b/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py @@ -789,7 +789,7 @@ def createAppliedSchemasSection(self): # so we can later display them into the same UI section. # # We do not group the collections API. We rather create a section that - # includes the instance in the title. This allows up to optimally trim + # includes the instance in the title. This allows us to optimally trim # the type name and the namespace when generating the attribute nice names. # # By example, on a UsdLux light, we have two UsdCollectionAPI applied. diff --git a/lib/mayaUsd/ufe/UsdAttributeHolder.cpp b/lib/mayaUsd/ufe/UsdAttributeHolder.cpp index 757b09c96b..ad0e92671d 100644 --- a/lib/mayaUsd/ufe/UsdAttributeHolder.cpp +++ b/lib/mayaUsd/ufe/UsdAttributeHolder.cpp @@ -290,7 +290,7 @@ Ufe::Value UsdAttributeHolder::getMetadata(const std::string& key) const prefix = prefix.substr(0, dunderPos) + typeNameAndInstance.second.GetString() + ":"; - // If the parameter name does not start with the templte, it does not belong to + // If the parameter name does not start with the template, it does not belong to // this API: if (_usdAttr.GetName().GetString().rfind(prefix, 0) == std::string::npos) { continue;