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

EMSUSD-1973 Add LookdevXUsd unit tests #4113

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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
7 changes: 7 additions & 0 deletions cmake/modules/FindLookdevXUfe.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,10 @@ find_package_handle_standard_args(LookdevXUfe
VERSION_VAR
LookdevXUfe_VERSION
)

set(LOOKDEVXUFE_HAS_PYTHON_BINDINGS FALSE CACHE INTERNAL "PyLookdevXUfe")
if(LookdevXUfe_VERSION VERSION_GREATER_EQUAL "1.0.1" OR
(LookdevXUfe_VERSION VERSION_LESS "1.0.0" AND LookdevXUfe_VERSION VERSION_GREATER_EQUAL "0.2.0"))
set(LOOKDEVXUFE_HAS_PYTHON_BINDINGS TRUE CACHE INTERNAL "PyLookdevXUfe")
message(STATUS "Maya has LookdevXUfe Python bindings")
endif()
92 changes: 24 additions & 68 deletions lib/lookdevXUsd/UsdFileHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,11 @@

#include <mayaUsdAPI/utils.h>

#include <pxr/usd/sdf/layerUtils.h>
#include <pxr/usd/usdShade/udimUtils.h>

#include <LookdevXUfe/UfeUtils.h>

#include <algorithm>
#include <string_view>

namespace LookdevXUsd
{
Expand All @@ -44,68 +43,20 @@ std::string getRelativePath(const Ufe::AttributeFilename::Ptr& fnAttr, const std
return path;
}

// This function is basically:
// return prefix + middle + suffix;
// But in a way that allocates memory only once and keeps clang-tidy happy.
// See: performance-inefficient-string-concatenation in clang-tidy docs.
std::string buildFullPath(const std::string_view& prefix,
const std::string_view& middle,
const std::string_view& suffix)
{
std::string path;
path.reserve(prefix.size() + middle.size() + suffix.size() + 1);
path.append(prefix);
path.append(middle);
path.append(suffix);
return path;
}

std::string resolveUdimPath(const std::string& path, const Ufe::Attribute::Ptr& attribute)
{
constexpr auto kUdimMin = 1001;
constexpr auto kUdimMax = 1100;
constexpr auto kUdimNumSize = 4;
constexpr std::string_view kUdimTag = "<UDIM>";

const auto udimPos = path.rfind(kUdimTag);
if (udimPos == std::string::npos)
{
return {};
}

const auto stage = MayaUsdAPI::usdStage(attribute);
if (!stage || !stage->GetEditTarget().GetLayer())
{
return {};
}

// Going for minimal alloc code using string_view to the fullest :)
const auto udimPrefix = std::string_view{path.data(), udimPos};
const auto udimEndPos = udimPos + kUdimTag.size();

const auto udimPostfix =
std::string_view{path.data() + static_cast<ptrdiff_t>(udimEndPos), path.size() - udimEndPos};

// Build numericPath once and re-use it in the loop:
auto numericPath = buildFullPath(udimPrefix, std::to_string(kUdimMin), udimPostfix);
const auto numericStart = numericPath.begin() + static_cast<std::string::difference_type>(udimPrefix.size());
const auto numericEnd = numericStart + static_cast<std::string::difference_type>(kUdimNumSize);

for (auto i = kUdimMin; i < kUdimMax; ++i)
{
numericPath.replace(numericStart, numericEnd, std::to_string(i));

const auto resolved =
PXR_NS::SdfComputeAssetPathRelativeToLayer(stage->GetEditTarget().GetLayer(), numericPath);
if (resolved.size() != numericPath.size())
{
// Restore the <UDIM> tag in the resolved path:
const auto prefixLength = resolved.size() - udimPostfix.size() - kUdimNumSize;
return buildFullPath({resolved.data(), prefixLength}, kUdimTag, udimPostfix);
// From USD's materialParamsUtil.cpp:
// We need to find the first layer that changes the value
// of the parameter so that we anchor relative paths to that.
static
PXR_NS::SdfLayerHandle
findLayerHandle(const PXR_NS::UsdAttribute& attr, const PXR_NS::UsdTimeCode& time) {
for (const auto& spec: attr.GetPropertyStack(time)) {
if (spec->HasDefaultValue() ||
spec->GetLayer()->GetNumTimeSamplesForPath(
spec->GetPath()) > 0) {
return spec->GetLayer();
}
}

return {};
return PXR_NS::TfNullPtr;
}

} // namespace
Expand All @@ -123,15 +74,20 @@ std::string UsdFileHandler::getResolvedPath(const Ufe::AttributeFilename::Ptr& f
auto attributeType = MayaUsdAPI::usdAttributeType(fnAttr);
if (attributeType == PXR_NS::SdfValueTypeNames->Asset)
{
PXR_NS::VtValue vt;
if (MayaUsdAPI::getUsdValue(fnAttr, vt, MayaUsdAPI::getTime(fnAttr->sceneItem()->path())) &&
vt.IsHolding<PXR_NS::SdfAssetPath>())
const auto prim = MayaUsdAPI::getPrimForUsdSceneItem(fnAttr->sceneItem());
const auto usdAttribute = prim.GetAttribute(PXR_NS::TfToken(fnAttr->name()));
const auto attrQuery = PXR_NS::UsdAttributeQuery(usdAttribute);
const auto time = MayaUsdAPI::getTime(fnAttr->sceneItem()->path());
PXR_NS::SdfAssetPath assetPath;

if (attrQuery.Get(&assetPath, time))
{
const auto& assetPath = vt.UncheckedGet<PXR_NS::SdfAssetPath>();
auto path = assetPath.GetResolvedPath();
if (path.empty())
if (path.empty() && PXR_NS::UsdShadeUdimUtils::IsUdimIdentifier(assetPath.GetAssetPath()))
{
path = resolveUdimPath(assetPath.GetAssetPath(), fnAttr);
path =
PXR_NS::UsdShadeUdimUtils::ResolveUdimPath(
assetPath.GetAssetPath(), findLayerHandle(usdAttribute, time));
}
#ifdef _WIN32
std::replace(path.begin(), path.end(), '\\', '/');
Expand Down
10 changes: 10 additions & 0 deletions test/lib/ufe/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,16 @@ else()
set(UFE_HAS_CODE_WRAPPER "0")
endif()

if(LOOKDEVXUFE_HAS_PYTHON_BINDINGS)
list(APPEND TEST_SCRIPT_FILES
testLdxCapabilityHandler.py
testLdxComponentConnections.py
testLdxConnection.py
testLdxDebugHandler.py
testLdxFileHandler.py
)
endif()

foreach(script ${TEST_SCRIPT_FILES})
mayaUsd_get_unittest_target(target ${script})
mayaUsd_add_test(${target}
Expand Down
73 changes: 73 additions & 0 deletions test/lib/ufe/testLdxCapabilityHandler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env python
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it make more sense to have its own test folder for lookdevXUsd project ?


#
# Copyright 2025 Autodesk
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import fixturesUtils
import mayaUtils
import ufeUtils

from maya import cmds
from maya import standalone

import ufe
from ufe.extensions import lookdevXUfe # Not using "as" in this module.
import mayaUsd.ufe

import os
import unittest


class CapabilityHandlerTestCase(unittest.TestCase):
'''Verify the CapabilityHandler USD implementation.
'''

pluginsLoaded = False

@classmethod
def setUpClass(cls):
fixturesUtils.readOnlySetUpClass(__file__, loadPlugin=False)

if not cls.pluginsLoaded:
cls.pluginsLoaded = mayaUtils.isMayaUsdPluginLoaded()

@classmethod
def tearDownClass(cls):
standalone.uninitialize()

def setUp(self):
''' Called initially to set up the Maya test environment '''
# Load plugins
self.assertTrue(self.pluginsLoaded)

# Open ballset.ma scene in testSamples
mayaUtils.openGroupBallsScene()

# Clear selection to start off
cmds.select(clear=True)

def testCapabilities(self):
rid = ufe.RunTimeMgr.instance().getId('USD')
ldxCaps = ufe.extensions.lookdevXUfe.CapabilityHandler.get(rid)
self.assertIsNotNone(ldxCaps)

self.assertTrue(ldxCaps.hasCapability(ufe.extensions.lookdevXUfe.CapabilityHandler.kCanPromoteToMaterial))
self.assertTrue(ldxCaps.hasCapability(ufe.extensions.lookdevXUfe.CapabilityHandler.kCanPromoteInputAtTopLevel))
self.assertTrue(ldxCaps.hasCapability(ufe.extensions.lookdevXUfe.CapabilityHandler.kCanHaveNestedNodeGraphs))
self.assertFalse(ldxCaps.hasCapability(ufe.extensions.lookdevXUfe.CapabilityHandler.kCanUseCreateShaderCommandForComponentConnections))

if __name__ == '__main__':
unittest.main(verbosity=2)
Loading