diff --git a/plugin/adsk/scripts/CMakeLists.txt b/plugin/adsk/scripts/CMakeLists.txt index 1f4144100..15d58421b 100644 --- a/plugin/adsk/scripts/CMakeLists.txt +++ b/plugin/adsk/scripts/CMakeLists.txt @@ -41,3 +41,7 @@ configure_file("${PROJECT_SOURCE_DIR}/lib/usd/pxrUsdPreviewSurface/AEusdPreviewS install(FILES ${CMAKE_CURRENT_BINARY_DIR}/AEusdPreviewSurfaceTemplate.mel DESTINATION ${INSTALL_DIR_SUFFIX}/scripts ) + +install(FILES RunUsdTool.py + DESTINATION ${CMAKE_INSTALL_PREFIX}/bin +) diff --git a/plugin/adsk/scripts/RunUsdTool.py b/plugin/adsk/scripts/RunUsdTool.py new file mode 100644 index 000000000..52825d46a --- /dev/null +++ b/plugin/adsk/scripts/RunUsdTool.py @@ -0,0 +1,105 @@ +import os.path +import os +import sys +import subprocess +from typing import List + + +def run_usd_tool(): + '''Setup the dynamic libraries paths and run the specified USD tool command''' + + if len(sys.argv) < 2: + print('Usage: RunUsdTool.py [args...]') + sys.exit(1) + + tool_name = sys.argv[1] + + # Find where the current script is located to find other files relative to it + script_folder = os.path.dirname(__file__) + + # Find the Maya module file (.mod) to get the libraries paths + mod_files = find_mod_files(os.path.join(script_folder, '..', '..', '..')) + pretty_mod_files = "\n".join([' ' + mod_file for mod_file in mod_files]) + print(f'Found Maya module files:\n{pretty_mod_files}') + + # Find all the dynamic libraries folders from the module files + lib_folders = [] + for mod_file in mod_files: + lib_folders.extend(find_mod_file_libraries_folders(mod_file)) + pretty_lib_folders = "\n".join([' ' + lib_folder for lib_folder in lib_folders]) + print(f'Found dynamic libraries folders:\n{pretty_lib_folders}') + + # Update the OS environment variable to include the dynamic libraries paths + update_os_dylib_env_var(lib_folders) + + # Run the specified tool executable with the remaining arguments + cmd = tool_name + args = [cmd] + sys.argv[2:] + subprocess.run(args) + + +def find_mod_files(folder) -> List[str]: + '''Find Maya plugin module file to find dynamic libraries paths''' + mod_files = [] + for current_folder, sub_folders, sub_files in os.walk(folder): + for sub_file in sub_files: + if not sub_file.endswith('.mod'): + continue + mod_files.append(os.path.normpath(os.path.join(current_folder, sub_file))) + return mod_files + + +def find_mod_file_libraries_folders(mod_file_name) -> List[str]: + '''Find plugin installation paths and add the corresponding dynamic libraries''' + found_lib_folders = [] + mod_file_folder = os.path.dirname(mod_file_name) + + if not os.path.exists(mod_file_name): + print('Cannot find the MayaUSD module file.') + return found_lib_folders + + for line in open(mod_file_name).readlines(): + if not line.startswith('+'): + continue + current_folder = extract_plugin_folder(line) + + # Construct the path to the USD dynamic libraries directory + # Update the environment variable to include the libraries path + for lib_folder in ['lib', 'lib64']: + dylib_folder = os.path.normpath(os.path.join(mod_file_folder, current_folder, lib_folder)) + if os.path.exists(dylib_folder): + found_lib_folders.append(dylib_folder) + + return found_lib_folders + + +def update_os_dylib_env_var(lib_folders): + '''Update the OS environment variable to include the dynamic libraries paths''' + env_var = get_os_dylib_env_var() + for lib_folder in lib_folders: + if os.path.exists(lib_folder): + if env_var in os.environ: + os.environ[env_var] = lib_folder + os.pathsep + os.environ[env_var] + else: + os.environ[env_var] = lib_folder + print(f'Updated {env_var} to: {os.environ.get(env_var)}\n\n') + + +def get_os_dylib_env_var(): + '''Determine which environment variable to modify''' + if sys.platform.startswith("win"): + return 'PATH' + elif sys.platform.startswith("linux"): + return 'LD_LIBRARY_PATH' + elif sys.platform == "darwin": + return 'DYLD_LIBRARY_PATH' + else: + return 'PATH' + + +def extract_plugin_folder(line): + '''Extract the Maya plugin folder from the plugin declaration''' + return line.strip().split(' ')[-1] + + +run_usd_tool() diff --git a/test/lib/CMakeLists.txt b/test/lib/CMakeLists.txt index 88fe38b64..e1bedc089 100644 --- a/test/lib/CMakeLists.txt +++ b/test/lib/CMakeLists.txt @@ -72,7 +72,7 @@ if (AdskUsdEditForward_FOUND) WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} PYTHON_SCRIPT ${script} ENV - "LD_LIBRARY_PATH=${ADDITIONAL_LD_LIBRARY_PATH}" + "LD_LIBRARY_PATH=${ADDITIONAL_LD_LIBRARY_PATH}:${PXR_USD_LOCATION}/lib" "MAYA_PLUG_IN_PATH=${CMAKE_INSTALL_PREFIX}/lib/maya" "PXR_OVERRIDE_PLUGINPATH_NAME=${CMAKE_INSTALL_PREFIX}/lib/usd" ) diff --git a/test/lib/testAdskUsdComponentCreator.py b/test/lib/testAdskUsdComponentCreator.py index 5f6db2c2e..ea186befd 100644 --- a/test/lib/testAdskUsdComponentCreator.py +++ b/test/lib/testAdskUsdComponentCreator.py @@ -93,6 +93,393 @@ def testPluginLoadable(self): self.assertTrue(is_cc_init, "Component Creator was not initialized but MAYAUSD_FORCE_CC_TEST is set.") + +class _ComponentCreatorTestBase: + """Shared helpers for component creator test cases.""" + + def _setUpCC(self): + """Load required plugins for CC tests. Call from setUp().""" + if not _CC_AVAILABLE: + self.skipTest('Could not find the USD component creator plugin') + mayaUtils.loadPlugin('mayaUsdPlugin') + if _HAVE_CC_MAYA_PLUGIN: + mayaUtils.loadPlugin('usd_component_creator') + else: + # See comment in LoadComponentCreatorPluginTestCase.testPluginLoadable + cmds.flushIdleQueue(resume=True) + cmds.flushIdleQueue() + cmds.file(new=True, force=True) + + def _snapshotProxyShapes(self): + """Return the set of all mayaUsdProxyShape node paths currently in the scene.""" + return set(cmds.ls(type='mayaUsdProxyShape', long=True) or []) + + def _findNewProxyShape(self, before): + """Return the first proxy shape that was added since *before* was captured.""" + new = self._snapshotProxyShapes() - before + return list(new)[0] if new else None + + def _getActiveDesc(self): + """Return the ComponentDescription currently shown in the variant editor, or None.""" + from usd_component_creator_plugin import get_variant_editor_component_description + return get_variant_editor_component_description() + + def _getDescFromStage(self, stage): + """Create a ComponentDescription from a USD stage's metadata.""" + import AdskUsdComponentCreator + return AdskUsdComponentCreator.ComponentDescription.CreateFromStageMetadata(stage) + +# --------------------------------------------------------------------------- +# Test: create_component_from_nodes +# --------------------------------------------------------------------------- + +class CreateComponentFromNodesTestCase(_ComponentCreatorTestBase, unittest.TestCase): + """ + Tests for usd_component_creator_plugin.create_component.create_component_from_nodes. + """ + + @classmethod + def setUpClass(cls): + fixturesUtils.readOnlySetUpClass(__file__, initializeStandalone=False) + + def setUp(self): + self._setUpCC() + + def testEmptyNodesIsNoOp(self): + """create_component_from_nodes([]) must not create a proxy shape or raise.""" + from usd_component_creator_plugin import create_component_from_nodes + before = self._snapshotProxyShapes() + create_component_from_nodes([]) + self.assertEqual(self._snapshotProxyShapes(), before, + "No proxy shape should be created for an empty node list") + + def testGeometryInStage(self): + """The exported geometry should appear under /root/geo/ in the stage.""" + from usd_component_creator_plugin import create_component_from_nodes + cmds.polyCube(name='pCube1') + path = cmds.ls('pCube1', long=True)[0] + before = self._snapshotProxyShapes() + create_component_from_nodes([path]) + proxy = self._findNewProxyShape(before) + self.assertIsNotNone(proxy) + stage = mayaUsd.ufe.getStage(proxy) + geo_prim = stage.GetPrimAtPath('/root/geo/pCube1') + self.assertTrue(geo_prim.IsValid(), + "pCube1 prim should exist under /root/geo in the component stage") + + def testHasVariantSet(self): + """The resulting component should have at least one variant set with one variant.""" + from usd_component_creator_plugin import create_component_from_nodes + cmds.polyCube(name='pCube1') + path = cmds.ls('pCube1', long=True)[0] + before = self._snapshotProxyShapes() + create_component_from_nodes([path]) + proxy = self._findNewProxyShape(before) + self.assertIsNotNone(proxy) + stage = mayaUsd.ufe.getStage(proxy) + desc = self._getDescFromStage(stage) + self.assertIsNotNone(desc, 'Could not construct ComponentDescription from stage') + variant_sets = desc.GetVariantSets() + self.assertEqual(len(variant_sets), 1, + "Component must have one variant set") + first_vs = next(iter(variant_sets.values())) + self.assertEqual(len(first_vs.GetVariants()), 1, + "Variant set must have one variant") + + def testDefaultAndTargetVariantIsSet(self): + """The first variant set must have a non-empty default_variant after creation.""" + from usd_component_creator_plugin import create_component_from_nodes + import AdskUsdComponentCreator + cmds.polyCube(name='pCube1') + path = cmds.ls('pCube1', long=True)[0] + before = self._snapshotProxyShapes() + create_component_from_nodes([path]) + proxy = self._findNewProxyShape(before) + self.assertIsNotNone(proxy) + stage = mayaUsd.ufe.getStage(proxy) + desc = self._getDescFromStage(stage) + self.assertIsNotNone(desc, 'Could not construct ComponentDescription from stage') + first_vs = next(iter(desc.GetVariantSets().values())) + self.assertTrue(first_vs.default_variant, + "Variant set must have a default variant defined after creation") + default_var_desc = first_vs.GetVariantDescription(first_vs.default_variant) + self.assertIsNotNone(default_var_desc, + "GetVariantDescription must return a descriptor for the default variant") + self.assertTrue( + AdskUsdComponentCreator.ComponentAPI.IsVariantTarget( + desc, [first_vs, default_var_desc]), + "Default variant must be the targeted variant after create_component_from_nodes " + "(target_default_variant=True)") + +# --------------------------------------------------------------------------- +# Test: create_multi_variants_component_from_nodes +# --------------------------------------------------------------------------- + +class CreateMultiVariantsComponentFromNodesTestCase(_ComponentCreatorTestBase, unittest.TestCase): + """ + Tests for create_multi_variants_component_from_nodes. + """ + + @classmethod + def setUpClass(cls): + fixturesUtils.readOnlySetUpClass(__file__, initializeStandalone=False) + + def setUp(self): + self._setUpCC() + + def testEmptyNodesIsNoOp(self): + """create_multi_variants_component_from_nodes([]) must not create any proxy shape.""" + from usd_component_creator_plugin import create_multi_variants_component_from_nodes + before = self._snapshotProxyShapes() + create_multi_variants_component_from_nodes([]) + self.assertEqual(self._snapshotProxyShapes(), before, + "No proxy shape should be created for an empty node list") + + def testVariantCount(self): + """Two input nodes should produce exactly two variants in the first variant set.""" + from usd_component_creator_plugin import create_multi_variants_component_from_nodes + cmds.polyCube(name='pCubeA') + cmds.polyCube(name='pCubeB') + paths = [cmds.ls(n, long=True)[0] for n in ('pCubeA', 'pCubeB')] + before = self._snapshotProxyShapes() + create_multi_variants_component_from_nodes(paths) + proxy = self._findNewProxyShape(before) + self.assertIsNotNone(proxy) + stage = mayaUsd.ufe.getStage(proxy) + desc = self._getDescFromStage(stage) + self.assertIsNotNone(desc, 'Could not construct ComponentDescription from stage') + first_vs = next(iter(desc.GetVariantSets().values())) + self.assertEqual(len(first_vs.GetVariants()), 2, + "Two nodes must produce exactly two variants") + + def testTargetedVariantIsDefault(self): + """After creation the targeted variant must be the default variant.""" + import AdskUsdComponentCreator + from usd_component_creator_plugin import create_multi_variants_component_from_nodes + cmds.polyCube(name='ZebraNode') + cmds.polyCube(name='AppleNode') + paths = [cmds.ls(n, long=True)[0] for n in ('ZebraNode', 'AppleNode')] + before = self._snapshotProxyShapes() + create_multi_variants_component_from_nodes(paths) + proxy = self._findNewProxyShape(before) + self.assertIsNotNone(proxy) + + desc = self._getActiveDesc() + if desc is None: + stage = mayaUsd.ufe.getStage(proxy) + desc = self._getDescFromStage(stage) + self.assertIsNotNone(desc, 'Could not obtain ComponentDescription') + + first_vs = next(iter(desc.GetVariantSets().values())) + default_name = first_vs.default_variant + self.assertTrue(default_name) + default_var_desc = first_vs.GetVariantDescription(default_name) + self.assertIsNotNone(default_var_desc) + self.assertTrue( + AdskUsdComponentCreator.ComponentAPI.IsVariantTarget( + desc, [first_vs, default_var_desc]), + "Default variant must be targeted after create_multi_variants_component_from_nodes") + + def testDefaultIsAlphabeticallyFirst(self): + """With three nodes the alphabetically-first name should be the default variant.""" + import AdskUsdComponentCreator + from usd_component_creator_plugin import create_multi_variants_component_from_nodes + for name in ('MangoNode', 'AvocadoNode', 'KiwiNode'): + cmds.polyCube(name=name) + paths = [cmds.ls(n, long=True)[0] for n in ('MangoNode', 'AvocadoNode', 'KiwiNode')] + before = self._snapshotProxyShapes() + create_multi_variants_component_from_nodes(paths) + proxy = self._findNewProxyShape(before) + self.assertIsNotNone(proxy) + stage = mayaUsd.ufe.getStage(proxy) + desc = self._getDescFromStage(stage) + self.assertIsNotNone(desc, 'Could not construct ComponentDescription from stage') + + first_vs = next(iter(desc.GetVariantSets().values())) + self.assertEqual(len(first_vs.GetVariants()), 3, "Three nodes → three variants") + + opts = AdskUsdComponentCreator.Options() + opts.Validate() + expected_name = AdskUsdComponentCreator.GenerateVariantNameFromObjectName( + opts, 'AvocadoNode', ['AvocadoNode'], []) + self.assertEqual(first_vs.default_variant, expected_name, + "AvocadoNode is alphabetically first and must be the default variant") + + +# --------------------------------------------------------------------------- +# Test: add_to_component_from_nodes +# --------------------------------------------------------------------------- + +class AddToComponentFromNodesTestCase(_ComponentCreatorTestBase, unittest.TestCase): + """ + Tests for usd_component_creator_plugin.create_component.add_to_component_from_nodes. + """ + + @classmethod + def setUpClass(cls): + fixturesUtils.readOnlySetUpClass(__file__, initializeStandalone=False) + + def setUp(self): + self._setUpCC() + # Clear the variant editor state so there is no lingering component from a + # previous test. + #from usd_component_creator_plugin import update_variant_editor_window + #update_variant_editor_window(None, force_update=True) + + def _createInitialComponent(self, node_name='pCube1'): + """Create a polyCube, build a single-node component, and return (proxy, desc).""" + from usd_component_creator_plugin import create_component_from_nodes + cmds.polyCube(name=node_name) + path = cmds.ls(node_name, long=True)[0] + before = self._snapshotProxyShapes() + create_component_from_nodes([path]) + proxy = self._findNewProxyShape(before) + desc = self._getActiveDesc() + return proxy, desc + + def testNoComponentDescReturnsFalse(self): + """add_to_component_from_nodes with no desc and empty variant editor returns False.""" + from usd_component_creator_plugin import add_to_component_from_nodes + cmds.polyCube(name='pCube1') + path = cmds.ls('pCube1', long=True)[0] + result = add_to_component_from_nodes([path], [('model', 'extra')], + is_replacing=False, component_desc=None) + self.assertFalse(result, + "add_to_component_from_nodes must return False when no component " + "description is available") + + def testSanityReturnsTrueNoNewProxy(self): + """add_to_component_from_nodes returns True and reuses the existing proxy shape.""" + from usd_component_creator_plugin import add_to_component_from_nodes + proxy, desc = self._createInitialComponent('pCube1') + self.assertIsNotNone(desc, 'Could not get initial ComponentDescription') + first_vs = next(iter(desc.GetVariantSets().values())) + + cmds.polyCube(name='pCube2') + path2 = cmds.ls('pCube2', long=True)[0] + before = self._snapshotProxyShapes() + + # Add pCube2 to the existing default variant (not a new one) + result = add_to_component_from_nodes( + [path2], + [(first_vs.name, first_vs.default_variant)], + is_replacing=False, + component_desc=desc) + self.assertTrue(result, "add_to_component_from_nodes should return True on success") + new_proxy = self._findNewProxyShape(before) + self.assertIsNone(new_proxy, "No new proxy shape should be created when adding to an existing component") + + def testDefaultVariantUnchanged(self): + """Adding a node to an existing variant must not change the default_variant name.""" + from usd_component_creator_plugin import add_to_component_from_nodes + proxy, desc = self._createInitialComponent('pCube1') + self.assertIsNotNone(desc, 'Could not get initial ComponentDescription') + first_vs = next(iter(desc.GetVariantSets().values())) + original_default = first_vs.default_variant + self.assertTrue(original_default, "There must be a default variant before the add") + + cmds.polyCube(name='pCube2') + path2 = cmds.ls('pCube2', long=True)[0] + + result = add_to_component_from_nodes( + [path2], + [(first_vs.name, original_default)], + is_replacing=False, + component_desc=desc) + self.assertTrue(result) + + updated_desc = self._getActiveDesc() + self.assertIsNotNone(updated_desc, 'Could not get updated ComponentDescription') + updated_vs = next(iter(updated_desc.GetVariantSets().values())) + self.assertEqual(updated_vs.default_variant, original_default, + "default_variant must not change after add_to_component_from_nodes " + "(is_default_variant=False)") + + def testDefaultVariantUnchangedWhenAddingToNonDefault(self): + """Adding a node to a non-default variant must not change the default_variant name.""" + import AdskUsdComponentCreator + from usd_component_creator_plugin import ( + add_to_component_from_nodes, + create_multi_variants_component_from_nodes, + ) + # Create a 2-variant component: 'AppleNode' is alphabetically first -> default variant. + for name in ('ZebraNode', 'AppleNode'): + cmds.polyCube(name=name) + paths = [cmds.ls(n, long=True)[0] for n in ('ZebraNode', 'AppleNode')] + before = self._snapshotProxyShapes() + create_multi_variants_component_from_nodes(paths) + proxy = self._findNewProxyShape(before) + self.assertIsNotNone(proxy) + desc = self._getActiveDesc() + self.assertIsNotNone(desc, 'Could not get ComponentDescription after multi-variant create') + + first_vs = next(iter(desc.GetVariantSets().values())) + original_default = first_vs.default_variant + self.assertTrue(original_default, "There must be a default variant") + + # Pick the non-default variant to add to + non_default_name = next( + n for n in first_vs.GetVariants().keys() if n != original_default) + + cmds.polyCube(name='pCubeExtra') + path_extra = cmds.ls('pCubeExtra', long=True)[0] + + result = add_to_component_from_nodes( + [path_extra], + [(first_vs.name, non_default_name)], + is_replacing=False, + component_desc=desc) + self.assertTrue(result) + + updated_desc = self._getActiveDesc() + self.assertIsNotNone(updated_desc, 'Could not get updated ComponentDescription') + updated_vs = next(iter(updated_desc.GetVariantSets().values())) + self.assertEqual(updated_vs.default_variant, original_default, + "default_variant must not change when adding to a non-default variant " + "(is_default_variant=False)") + + def testTargetedVariantUnchangedAfterAdd(self): + """Adding a node to an existing variant must not change the targeted variant.""" + import AdskUsdComponentCreator + from usd_component_creator_plugin import add_to_component_from_nodes + proxy, desc = self._createInitialComponent('pCube1') + self.assertIsNotNone(desc, 'Could not get initial ComponentDescription') + first_vs = next(iter(desc.GetVariantSets().values())) + default_name = first_vs.default_variant + self.assertTrue(default_name) + default_var_desc = first_vs.GetVariantDescription(default_name) + self.assertIsNotNone(default_var_desc) + + # Verify: after create, default variant IS targeted + self.assertTrue( + AdskUsdComponentCreator.ComponentAPI.IsVariantTarget( + desc, [first_vs, default_var_desc]), + "Default variant must be targeted after create_component_from_nodes") + + # Add a second node to the same existing default variant + cmds.polyCube(name='pCube2') + path2 = cmds.ls('pCube2', long=True)[0] + result = add_to_component_from_nodes( + [path2], + [(first_vs.name, default_name)], + is_replacing=False, + component_desc=desc) + self.assertTrue(result) + + updated_desc = self._getActiveDesc() + if updated_desc is None: + stage = mayaUsd.ufe.getStage(proxy) + updated_desc = self._getDescFromStage(stage) + self.assertIsNotNone(updated_desc, 'Could not obtain updated ComponentDescription') + + updated_vs = next(iter(updated_desc.GetVariantSets().values())) + updated_default_var_desc = updated_vs.GetVariantDescription(default_name) + self.assertIsNotNone(updated_default_var_desc) + # Check target did not change + self.assertTrue( + AdskUsdComponentCreator.ComponentAPI.IsVariantTarget( + updated_desc, [updated_vs, updated_default_var_desc])) + class DuplicateToComponentTestCase(unittest.TestCase): """ Test duplicating Maya nodes to an Adsk USD Component stage via the diff --git a/test/lib/testAdskUsdEditForward.py b/test/lib/testAdskUsdEditForward.py index b2dd3455e..9fb90c00f 100644 --- a/test/lib/testAdskUsdEditForward.py +++ b/test/lib/testAdskUsdEditForward.py @@ -21,6 +21,7 @@ import time from pxr import Sdf, Usd +import AdskUsdEditForward from maya import cmds import maya.utils import mayaUsd @@ -61,22 +62,15 @@ def testEditForwarding(self): rootLayer.subLayerPaths.append(testLayer.identifier) # Configure forwarding to target layers matching .*TEST.* - # TODO : Would ideally use edit forwarding python API to configure the rule. - # Update the test when these are available. - customData = rootLayer.customLayerData - customData['adsk_forward_continuously'] = True - customData['adsk_forward_ruleset'] = { - 'rules': { - 'rule_0': { - 'description': 'test rule', - 'id': 'rule_0', - 'input_path_regex': '.*', - 'target_layer_regex': '.*TEST.*' - } - }, - 'version': 1 - } - rootLayer.customLayerData = customData + rule_def = AdskUsdEditForward.RuleDef() + rule_def.id = 'rule_0' + rule_def.description = 'test rule' + rule_def.input_object_expression = AdskUsdEditForward.RuleExpression('.*') + rule_def.target_layer_expression = AdskUsdEditForward.RuleExpression('.*TEST.*') + + rule_set = AdskUsdEditForward.RuleSet([rule_def]) + rule_set.continuous = True + AdskUsdEditForward.RuleDef.WriteRuleSetToLayerCustomData(rootLayer, rule_set) # Create a prim on the session layer via a UFE undoable command, so that # cmds.undo() can revert it along with the forward command as one unit.