Skip to content

Commit 5b77407

Browse files
EMSUSD-3530 save proxy shape of component as component
- Detect that a proxy shape is a component and ask the component creator to save it. Added unit tests: - Added unit tests to verify that the Maya save command saves components with variants correctly and the data is correct when reloaded. - The test needs to reload the stage because the saving bug would manifest itself only when fully reloading the stage, like when opening the scene in a fresh Maya session.
1 parent 6dbcf98 commit 5b77407

16 files changed

Lines changed: 955 additions & 33 deletions

lib/mayaUsd/nodes/layerManager.cpp

Lines changed: 53 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include <mayaUsd/undo/OpUndoItems.h>
2626
#include <mayaUsd/utils/layerMuting.h>
2727
#include <mayaUsd/utils/util.h>
28+
#include <mayaUsd/utils/utilComponentCreator.h>
2829
#include <mayaUsd/utils/utilFileSystem.h>
2930
#include <mayaUsd/utils/utilSerialization.h>
3031

@@ -1137,19 +1138,29 @@ BatchSaveResult LayerDatabase::saveUsdToMayaFile()
11371138
const StageSavingInfo& info = i < _proxiesToSave.size()
11381139
? _proxiesToSave[i]
11391140
: _internalProxiesToSave[i - _proxiesToSave.size()];
1141+
1142+
std::string proxyPath = info.dagPath.fullPathName().asChar();
1143+
if (MayaUsd::ComponentUtils::isAdskUsdComponent(proxyPath)) {
1144+
MayaUsd::ComponentUtils::saveAdskUsdComponent(proxyPath);
1145+
continue;
1146+
}
1147+
11401148
MObject mobj = info.dagPath.node();
11411149
fn.setObject(mobj);
1142-
if (!fn.isFromReferencedFile()
1143-
&& LayerDatabase::instance().supportedNodeType(fn.typeId())) {
1144-
1145-
// Here if its unshared or not an incoming connection we save otherwise skip
1146-
if (!info.shareable || !info.isIncoming) {
1147-
auto result = saveStageToMayaFile(lm, builder, mobj, info.stage);
1148-
if (result._stageHasDirtyLayers) {
1149-
atLeastOneDirty = true;
1150-
}
1151-
layersHandle.set(builder);
1150+
1151+
if (fn.isFromReferencedFile())
1152+
continue;
1153+
1154+
if (!LayerDatabase::instance().supportedNodeType(fn.typeId()))
1155+
continue;
1156+
1157+
// Here if its unshared or not an incoming connection we save otherwise skip
1158+
if (!info.shareable || !info.isIncoming) {
1159+
auto result = saveStageToMayaFile(lm, builder, mobj, info.stage);
1160+
if (result._stageHasDirtyLayers) {
1161+
atLeastOneDirty = true;
11521162
}
1163+
layersHandle.set(builder);
11531164
}
11541165
}
11551166

@@ -1174,32 +1185,41 @@ BatchSaveResult LayerDatabase::saveUsdToUsdFiles()
11741185
? _proxiesToSave[i]
11751186
: _internalProxiesToSave[i - _proxiesToSave.size()];
11761187

1188+
std::string proxyPath = info.dagPath.fullPathName().asChar();
1189+
if (MayaUsd::ComponentUtils::isAdskUsdComponent(proxyPath)) {
1190+
MayaUsd::ComponentUtils::saveAdskUsdComponent(proxyPath);
1191+
continue;
1192+
}
1193+
11771194
MObject mobj = info.dagPath.node();
11781195
fn.setObject(mobj);
1179-
if (!fn.isFromReferencedFile()
1180-
&& LayerDatabase::instance().supportedNodeType(fn.typeId())) {
1181-
MayaUsdProxyShapeBase* pShape = static_cast<MayaUsdProxyShapeBase*>(fn.userNode());
11821196

1183-
// Unshared Composition Saves to MayaFile Always
1184-
if (!info.shareable) {
1185-
saveStageToMayaFile(mobj, info.stage);
1186-
} else {
1187-
// No need to save stages from external sources
1188-
if (info.isIncoming) {
1189-
continue;
1190-
}
1191-
convertAnonymousLayers(pShape, mobj, info.stage);
1192-
const auto& sessionLayer = info.stage->GetSessionLayer();
1193-
for (const auto& layer : getSaveCandidateLayers(*info.stage)) {
1194-
if (TF_VERIFY(layer)) {
1195-
if (layer != sessionLayer && layer->PermissionToSave()
1196-
&& layer->IsDirty()) {
1197-
if (!MayaUsd::utils::saveLayerWithFormat(layer)) {
1198-
MString errMsg;
1199-
MString layerName(layer->GetDisplayName().c_str());
1200-
errMsg.format("Could not save layer ^1s.", layerName);
1201-
MGlobal::displayError(errMsg);
1202-
}
1197+
if (fn.isFromReferencedFile())
1198+
continue;
1199+
1200+
if (!LayerDatabase::instance().supportedNodeType(fn.typeId()))
1201+
continue;
1202+
1203+
MayaUsdProxyShapeBase* pShape = static_cast<MayaUsdProxyShapeBase*>(fn.userNode());
1204+
1205+
// Unshared Composition Saves to MayaFile Always
1206+
if (!info.shareable) {
1207+
saveStageToMayaFile(mobj, info.stage);
1208+
} else {
1209+
// No need to save stages from external sources
1210+
if (info.isIncoming) {
1211+
continue;
1212+
}
1213+
convertAnonymousLayers(pShape, mobj, info.stage);
1214+
const auto& sessionLayer = info.stage->GetSessionLayer();
1215+
for (const auto& layer : getSaveCandidateLayers(*info.stage)) {
1216+
if (TF_VERIFY(layer)) {
1217+
if (layer != sessionLayer && layer->PermissionToSave() && layer->IsDirty()) {
1218+
if (!MayaUsd::utils::saveLayerWithFormat(layer)) {
1219+
MString errMsg;
1220+
MString layerName(layer->GetDisplayName().c_str());
1221+
errMsg.format("Could not save layer ^1s.", layerName);
1222+
MGlobal::displayError(errMsg);
12031223
}
12041224
}
12051225
}

test/lib/componentCreator/CMakeLists.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ if (AdskUsdComponentCreator_FOUND)
1717
testComponentCreatorAddPrimInComponent.py
1818
)
1919

20+
set(COMPONENT_CREATOR_BATCH_TEST_SCRIPT_FILES
21+
testComponentCreatorSave.py
22+
)
23+
2024
foreach(script ${COMPONENT_CREATOR_TEST_SCRIPT_FILES})
2125
mayaUsd_get_unittest_target(target ${script})
2226
mayaUsd_add_test(${target}
@@ -35,6 +39,20 @@ if (AdskUsdComponentCreator_FOUND)
3539
)
3640
set_property(TEST ${target} APPEND PROPERTY LABELS SharedComponents)
3741
endforeach()
42+
43+
foreach(script ${COMPONENT_CREATOR_BATCH_TEST_SCRIPT_FILES})
44+
mayaUsd_get_unittest_target(target ${script})
45+
mayaUsd_add_test(${target}
46+
PYTHON_MODULE ${target}
47+
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
48+
ENV
49+
"USD_FORCE_DEFAULT_MATERIALS_SCOPE_NAME=1"
50+
"LD_LIBRARY_PATH=${ADDITIONAL_LD_LIBRARY_PATH}"
51+
"MAYA_MODULE_PATH=${CMAKE_INSTALL_PREFIX}/../AdskUsdComponentCreator"
52+
"PXR_OVERRIDE_PLUGINPATH_NAME=${CMAKE_INSTALL_PREFIX}/lib/usd"
53+
)
54+
set_property(TEST ${target} APPEND PROPERTY LABELS SharedComponents)
55+
endforeach()
3856
else()
3957
# If MAYAUSD_FORCE_CC_TEST is set to 1, fail the build when component creator is not found
4058
if (DEFINED ENV{MAYAUSD_FORCE_CC_TEST} AND "$ENV{MAYAUSD_FORCE_CC_TEST}" STREQUAL "1")

test/lib/componentCreator/testComponentCreatorBase.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ def _findNewProxyShape(self, before):
2929
new = self._snapshotProxyShapes() - before
3030
return list(new)[0] if new else None
3131

32+
def _openVariantEditor(self, component_description):
33+
from usd_component_creator_plugin import open_variant_editor_window
34+
from AdskVariantEditor import ComponentData
35+
compData = ComponentData(component_description) if component_description else None
36+
open_variant_editor_window(None)
37+
3238
def _getActiveDesc(self):
3339
"""Return the ComponentDescription currently shown in the variant editor, or None."""
3440
from usd_component_creator_plugin import get_variant_editor_component_description
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import unittest
2+
3+
import os.path
4+
import tempfile
5+
from distutils.dir_util import copy_tree
6+
7+
import fixturesUtils
8+
import testUtils
9+
import mayaUsd.lib
10+
import mayaUsd.ufe
11+
from maya import cmds
12+
from pxr import Sdf, Usd
13+
14+
from testComponentCreatorBase import _ComponentCreatorTestBase
15+
16+
class SaveToComponentTestCase(_ComponentCreatorTestBase, unittest.TestCase):
17+
"""
18+
Tests for usd_component_creator_plugin.create_component.add_to_component_from_nodes
19+
and then saving and reloading.
20+
21+
The reloading part is tested after in the test SaveToComponentFollowupTestCase.
22+
"""
23+
24+
_tempFolder = None
25+
26+
@classmethod
27+
def setUpClass(cls):
28+
fixturesUtils.readOnlySetUpClass(__file__, initializeStandalone=False)
29+
30+
testFolderName = "component3Variants"
31+
fromDirectory = testUtils.getTestScene(testFolderName)
32+
toDirectory = os.path.join(tempfile.gettempdir(), 'SaveToComponentTestCase')
33+
copy_tree(fromDirectory, toDirectory)
34+
SaveToComponentTestCase._tempFolder = toDirectory
35+
36+
@classmethod
37+
def tearDownClass(cls):
38+
if SaveToComponentTestCase._tempFolder:
39+
if os.path.exists(SaveToComponentTestCase._tempFolder):
40+
import shutil
41+
shutil.rmtree(SaveToComponentTestCase._tempFolder)
42+
cls._resetDefaultTemplate()
43+
return super().tearDownClass()
44+
45+
def _findVariantSet(self, desc, withName):
46+
variantSetMap = desc.GetVariantSets()
47+
self.assertGreaterEqual(len(variantSetMap), 1, "There must be at least one variant set")
48+
for variantSetName, variantSet in variantSetMap.items():
49+
if withName not in variantSetName:
50+
continue
51+
return variantSet
52+
return None
53+
54+
def setUp(self):
55+
self._setUpCC()
56+
# Clear the variant editor state so there is no lingering component from a
57+
# previous test.
58+
self._resetDefaultTemplate()
59+
return super().setUp()
60+
61+
def testSaveAndReload(self):
62+
"""
63+
Open a Maya scene containing a USD stage of a component.
64+
Modify all variants of the component by replacing its data with a new node.
65+
Save the scene. Re-open the scene and verify that the component is still
66+
valid and has the new data.
67+
"""
68+
before = self._snapshotProxyShapes()
69+
70+
mayaSceneFilePath = os.path.join(SaveToComponentTestCase._tempFolder, "repro-3530.ma")
71+
cmds.file(mayaSceneFilePath, open=True)
72+
73+
proxy = self._findNewProxyShape(before)
74+
self.assertIsNotNone(proxy)
75+
stage = mayaUsd.ufe.getStage(proxy)
76+
self.assertIsNotNone(stage)
77+
desc = self._getDescFromStage(stage)
78+
self.assertIsNotNone(desc, 'Could not get ComponentDescription from the Maya scene')
79+
80+
first_vs = self._findVariantSet(desc, 'variant_set_1')
81+
self.assertTrue(first_vs)
82+
self.assertGreaterEqual(len(first_vs.GetVariants()), 3, "There must be at least three variants in the set {}".format(first_vs.GetName()))
83+
84+
variantsMap = first_vs.GetVariants()
85+
self.assertGreaterEqual(len(variantsMap), 3, "There must be at least three variants")
86+
87+
for variantName in variantsMap.keys():
88+
polyCubeName = cmds.polyCube(name='pCubeExtra')[0]
89+
self.assertIn('pCubeExtra', polyCubeName)
90+
91+
# The following code is equivalent to add_to_component_from_nodes,
92+
# but that function does not work in maya batch mode and we need to run this test in batch mode
93+
# because the Maya save would pop-up a dialog in interactive mode.
94+
#
95+
# from usd_component_creator_plugin import add_to_component_from_nodes
96+
# result = add_to_component_from_nodes(
97+
# [polyCubeName],
98+
# [(first_vs.name, variantName)],
99+
# is_replacing=True,
100+
# component_desc=desc)
101+
# self.assertTrue(result)
102+
103+
from AdskUsdComponentCreator import CreateFromFileCommand
104+
from usd_component_creator_plugin import ExportNodesCommand, execute_ufe_command, AddComponentToManagerCommand, UfeCommandWrapper
105+
from usd_component_creator_plugin.create_component import _generate_unique_temp_filename, _update_options_for_node, _update_options_for_purpose_from_nodes
106+
107+
component_desc = desc
108+
nodes = [polyCubeName]
109+
variant_selections = [(first_vs.name, variantName)]
110+
is_replacing = True
111+
export_options = None
112+
purpose = None
113+
114+
options = component_desc.GetOptions().Clone()
115+
_update_options_for_node(options, nodes[0], nodes)
116+
options.component_variants = variant_selections if variant_selections else []
117+
options.replace_variant_content = is_replacing
118+
# Never change the default variant when appending to an existing component.
119+
options.is_default_variant = False
120+
121+
input_usd_filename = _generate_unique_temp_filename(options)
122+
export_cmd = ExportNodesCommand(nodes, input_usd_filename, export_options)
123+
execute_ufe_command(export_cmd)
124+
125+
creation_options = _update_options_for_purpose_from_nodes(options, purpose, input_usd_filename)
126+
creation_options.Validate()
127+
128+
delete_input_file = False
129+
create_cmd = CreateFromFileCommand(component_desc, input_usd_filename, creation_options, delete_input_file)
130+
self.assertTrue(UfeCommandWrapper.executeWithUndo(create_cmd))
131+
132+
add_cmd = AddComponentToManagerCommand(create_cmd)
133+
execute_ufe_command(add_cmd)
134+
135+
136+
updated_desc = self._getDescFromStage(stage)
137+
self.assertIsNotNone(updated_desc, 'Could not get updated ComponentDescription')
138+
139+
# Save the file. Make sure the USD edits will go to a USD file.
140+
cmds.optionVar(intValue=('mayaUsd_ConfirmExistingFileSave', 0))
141+
saveLocation = 1
142+
cmds.optionVar(intValue=(mayaUsd.lib.OptionVarTokens.SerializedUsdEditsLocation, saveLocation))
143+
cmds.file(save=True, force=True)
144+
145+
cmds.file(new=True, force=True)
146+
cmds.file(mayaSceneFilePath, open=True)
147+
148+
proxy = self._findNewProxyShape(before)
149+
self.assertIsNotNone(proxy)
150+
stage = mayaUsd.ufe.getStage(proxy)
151+
self.assertIsNotNone(stage)
152+
desc = self._getDescFromStage(stage)
153+
self.assertIsNotNone(desc, 'Could not get ComponentDescription from the Maya scene')
154+
155+
first_vs = self._findVariantSet(desc, 'variant_set_1')
156+
self.assertTrue(first_vs)
157+
158+
variantsMap = first_vs.GetVariants()
159+
self.assertGreaterEqual(len(variantsMap), 3, "There must be at least three variants")
160+
161+
variant_to_cube_map = {
162+
'pPlane1': 'pCubeExtra',
163+
'pPlane2': 'pCubeExtra1',
164+
'pPlane3': 'pCubeExtra2',
165+
}
166+
167+
found_cubes = set()
168+
169+
for variant in variantsMap.values():
170+
primPath = desc.root_prim_path
171+
prim = stage.GetPrimAtPath(primPath)
172+
self.assertTrue(prim)
173+
prim.GetVariantSet(first_vs.GetName()).SetVariantSelection(variant.GetName())
174+
175+
stage.Reload()
176+
177+
expected_cube_name = variant_to_cube_map.get(variant.GetName())
178+
self.assertIsNotNone(expected_cube_name, "No expected cube name for variant {}".format(variant.GetName()))
179+
180+
geoPrim = stage.GetPrimAtPath(Sdf.Path('/root/geo'))
181+
for child in geoPrim.GetChildren():
182+
print(f'Child: {child.GetName()}')
183+
if child.GetTypeName() == 'Mesh':
184+
self.assertEqual(expected_cube_name, child.GetName(), f"The variant {variant.GetName()} should have a cube named {expected_cube_name}")
185+
found_cubes.add(child.GetName())
186+
187+
self.assertEqual(len(found_cubes), len(variantsMap), "There should be one cube for each variant")
188+
189+
190+
if __name__ == '__main__':
191+
fixturesUtils.runTests(globals())
192+

0 commit comments

Comments
 (0)