Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Shorten nice names by trimming schema name #2818

Merged
merged 2 commits into from
Jan 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 15 additions & 12 deletions lib/mayaUsd/resources/ae/usdschemabase/ae_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 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.
# 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()
Expand All @@ -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
Expand Down
177 changes: 174 additions & 3 deletions lib/mayaUsd/ufe/UsdAttributeHolder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::pair<TfToken, const UsdPrimDefinition*>>;
DefVec defsToExplore;

const UsdSchemaRegistry& schemaReg = UsdSchemaRegistry::GetInstance();
for (auto&& name : _usdAttr.GetPrim().GetAppliedSchemas()) {

std::pair<TfToken, TfToken> 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<TfToken, TfToken> 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 template, 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)) {
Expand Down
42 changes: 41 additions & 1 deletion test/lib/ufe/testAttribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)