-
Notifications
You must be signed in to change notification settings - Fork 202
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
MAYA-99006 Filter out objects to export by hierarchy. #657
Conversation
Extracted the code to figure out the set of objects to export from the translator and commands into a single utility function. Added code to filter out any objects that also have parent objects in the list. This was previously an error in the write job and would error out.
if (argData.isFlagSet(kSelectionFlag)) { | ||
MGlobal::getActiveSelectionList(objSelList); | ||
} | ||
else { | ||
argData.getObjects(objSelList); | ||
|
||
// If no objects specified, then get all objects at DAG root | ||
if (objSelList.isEmpty()) { | ||
objSelList.add("|*", true); | ||
} | ||
} | ||
|
||
// Convert selection list to jobArgs dagPaths | ||
UsdMayaUtil::MDagPathSet dagPaths; | ||
for (unsigned int i=0; i < objSelList.length(); i++) { | ||
MDagPath dagPath; | ||
status = objSelList.getDagPath(i, dagPath); | ||
if (status == MS::kSuccess) | ||
{ | ||
dagPaths.emplace(dagPath); | ||
} | ||
bool exportSelected = argData.isFlagSet(kSelectionFlag); | ||
if (!exportSelected) { | ||
argData.getObjects(objSelList); | ||
} | ||
GetFilteredSelectionToExport(exportSelected, objSelList, dagPaths); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This bit where we get an object list and add to a set for export has been moved to util.h/.cpp as a utility function shared with the translator. The one thing that the command can do differently is get a list of objects explicitly passed to the command to work on.
// If we are in neither of these modes then there won't be anything to do | ||
if (mode != MPxFileTranslator::kExportActiveAccessMode && | ||
mode != MPxFileTranslator::kExportAccessMode) { | ||
return MS::kSuccess; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic farther down used to be that we would only fill the input list if one of these two flags were set. If neither were set then we had nothing to export and just returned kSuccess. Moving this check to the start since there is no point in continuing if we are in a different mode.
self.assertFalse(prim.IsValid()) | ||
|
||
if __name__ == '__main__': | ||
unittest.main(verbosity=2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test file just has a bunch of spheres parented in a hierarchy. The test selects things in a certain order where some objects lower in the hierarchy come first, which tests that we are correctly sorting the hierarchy in order to determine the top level ancestors to export. This was previously an error case with no file exported, now it correctly filters out the children, exports the file, and then we make sure we see the expected objects read back in from usd.
return strcmp(lhs.fullPathName().asChar(), rhs.fullPathName().asChar()) < 0; | ||
int pathCountDiff = lhs.pathCount() - rhs.pathCount(); | ||
return (0 != pathCountDiff) ? (pathCountDiff < 0) : | ||
(strcmp(lhs.fullPathName().asChar(), rhs.fullPathName().asChar()) < 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing the sorting function to more explicitly sort by hierarchy depth, and then alphabetically within the same depth. End result is the same because of the path separator used in the fullPathName, but adding the pathCount check makes it more obvious that it’s doing this. It would also be faster this way since the path count is a stored value internally so it’s a quick int compare in a lot of cases, avoiding a bunch of string compares.
@@ -2101,3 +2133,48 @@ UsdMayaUtil::nameToDagPath(const std::string& name) | |||
CHECK_MSTATUS(status); | |||
return dag; | |||
} | |||
|
|||
void | |||
UsdMayaUtil::GetFilteredSelectionToExport(bool exportSelected, MSelectionList& objectList, UsdMayaUtil::MDagPathSet& dagPaths) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had hoped that there was an easy API call or Maya command to do what I want here but I never found anything. Most of the code I found that did something similar ended up traversing the entire Maya scene using various dag iterators, sometimes multiple times, and it didn’t fit what we want to do here very well.
What I ended up doing was:
- Gather input in a MSelectionList
- Move the selection list into a set, sorted by hierarchy depth.
- Iterate over that set, so kind of depth first traversal of the input, not the entire Maya scene, moving objects we want to keep into a second set that will be returned to the caller.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks fine as it is, and I realize you didn't add a lot of code in utils/util.cpp, but I wonder if it wouldn't have been simpler to build up a Ufe::Selection and just use Ufe::Selection::containsAncestor().
namespace { | ||
bool shouldAddToSet(const MDagPath& toAdd, const UsdMayaUtil::MDagPathSet& dagPaths) | ||
// Utility function to check if an object should be added to the set of objects to | ||
// export. An object should not be added if it's invalid, or if any of it's parent |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"it's" --> "its"
Yes, you did mention using a Selection and I took a look at it. The one thing Selection has for it would be performance, the lookup in that function should be faster than a set lookup, but it felt like the amount of code required to use it could be quite a bit more. We already have to go from MSelectionList -> MDagPathSet, so the addition of an intermediate MDagPathSet doesn’t add much code that seems out of place (i.e. dag path is the core of both of those). To use a Selection…I’d have to convert dag paths to Ufe::Paths, create Ufe::SceneItems, fill up a Ufe::Selection…at that point, yes the lookup while iterating over SceneItems would be faster. I wouldn’t be against using it if performance was an issue, but this is already a relatively slow operation. |
I think it's fine as is, you had a look at using Ufe::Selection and decided against it, the resulting code size is small and fairly easy to understand, I doubt that we'll revisit this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a few small comments, but otherwise looks good to me!
} | ||
GetFilteredSelectionToExport(exportSelected, objSelList, dagPaths); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a dumb question, but how are we finding GetFilteredSelectionToExport()
without the UsdMayaUtil
namespace on the front?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm...all I know is it compiles and works somehow. I think I assumed that it was one of the "using" macros we add at the top of some of these files, but in this case it isn't that. I'll add it.
lib/mayaUsd/utils/util.h
Outdated
/// If \p exportSelected is true then the active selection list will be used to fill \p dagPaths | ||
/// with the objects to be exported. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might also mention here that the selection passed in with objectList
is ignored and it is re-populated with the active selection, just to clarify that objectList
is still mutated in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll update the comments to add something that mentions it.
if (exportSelected) { | ||
MGlobal::getActiveSelectionList(objectList); | ||
} else if (objectList.isEmpty()) { | ||
objectList.add("|*", true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hadn't noticed this before, but will this miss any top-level objects that have a namespace? Is there a syntax that would catch objects with arbitrarily deep namespaces?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|* will catch all top-level objects, including objects in a namespace. I could add a test for something like this though, can you give an example of what you mean by arbitrarily deep namespaces?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, sorry, nevermind this then. Apparently selection lists work differently from the ls
command. I remembered this being an issue with the latter, so it made me think the former might be affected as well.
I did this in the script editor to setup a test scene:
cmds.polyCube(name='Cube')
cmds.namespace(addNamespace=':NS_One')
cmds.namespace(setNamespace=':NS_One')
cmds.polyCube(name='Cube')
cmds.namespace(addNamespace=':NS_One:NS_Two')
cmds.namespace(setNamespace=':NS_One:NS_Two')
cmds.polyCube(name='Cube')
cmds.namespace(addNamespace=':NS_One:NS_Two:NS_Three')
cmds.namespace(setNamespace=':NS_One:NS_Two:NS_Three')
cmds.polyCube(name='Cube')
That yields four top-level nodes:
[n for n in cmds.ls(long=True) if n.endswith('Cube')]
# Result: [u'|Cube', u'|NS_One:Cube', u'|NS_One:NS_Two:Cube', u'|NS_One:NS_Two:NS_Three:Cube'] #
If I try to find those nodes using ls
though, I seem to have to explicitly add the right level of namespacing:
cmds.ls('|*')
# Result: [u'Cube', u'front', u'persp', u'side', u'top'] #
cmds.ls('|*:*')
# Result: [u'NS_One:Cube'] #
cmds.ls('|*:*:*')
# Result: [u'NS_One:NS_Two:Cube'] #
cmds.ls('|*:*:*:*')
# Result: [u'NS_One:NS_Two:NS_Three:Cube'] #
But the OpenMaya API indeed seems to behave the way you're describing:
from maya.api import OpenMaya as OM
objectList = OM.MSelectionList()
objectList.add('|*', True)
for i in range(objectList.length()):
print(objectList.getDagPath(i))
That yields (ignore the default cameras):
Cube
front
persp
side
top
NS_One:Cube
NS_One:NS_Two:Cube
NS_One:NS_Two:NS_Three:Cube
lib/mayaUsd/utils/util.cpp
Outdated
filterInput = false; | ||
} | ||
|
||
unsigned int nbObj = objectList.length(), i; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any particular benefit to declaring i
here versus in the for
loop? It looks a little odd to my eye hanging off the end of this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, at one point I had two for loops instead of just one that both used i. I'll put it back in the loop.
lib/mayaUsd/utils/util.cpp
Outdated
// Easiest way to filter by hierarchy is to: | ||
// 1. Put the input into a set that is sorted by distance from the world root. | ||
// 2. For each input object we iterate up its hierarchy checking if any parent is in the set. | ||
// 3. If no parent is in the set then we can add it. | ||
UsdMayaUtil::MDagPathSet sortedInput; | ||
for (i=0; i < nbObj; i++) { | ||
MDagPath dagPath; | ||
status = objectList.getDagPath(i, dagPath); | ||
if (status == MS::kSuccess) | ||
sortedInput.emplace(dagPath); | ||
} | ||
|
||
for(auto pIter : sortedInput) { | ||
if (!filterInput || shouldAddToSet(pIter, dagPaths)) | ||
dagPaths.emplace(pIter); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder whether this would be a useful utility in and of itself, sort of like SdfPath::RemoveDescendentPaths()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if I wanted to spend any more time on this then I'd probably choose to add some utility functions that make it easier to convert a Maya selection list to a Ufe Selection and then use its functionality to implement this. TBH I might leave it for now and come back to it if we ever find another reason to need it.
@@ -49,7 +49,6 @@ | |||
#include <maya/MPlug.h> | |||
#include <maya/MPlugArray.h> | |||
#include <maya/MPoint.h> | |||
#include <maya/MSelectionList.h> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We still use MSelectionList
in this file below as a param to GetFilteredSelectionToExport()
?
lib/mayaUsd/utils/util.h
Outdated
@@ -39,6 +38,7 @@ | |||
#include <maya/MPlug.h> | |||
#include <maya/MStatus.h> | |||
#include <maya/MString.h> | |||
#include <maya/MSelectionList.h> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move up before MStatus
:)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These MSelectionList changes actually came in during a rebase + fixing some merge conflicts...didn't even realize they'd moved.
requires maya "2020"; | ||
currentUnit -l centimeter -a degree -t film; | ||
fileInfo "application" "maya"; | ||
fileInfo "product" "Maya 2021"; | ||
fileInfo "version" "Preview Release 117"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, this is probably a pain, but any chance this test scene could be reconstructed using Maya 2019? If we're still claiming to support that version, that would ensure that people building for 2019 can still run this test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I was actually trying to fire up an older version of Maya to do exactly this but the two machines I have Maya on can't see each other when I'm on VPN and I forgot to come back to it. I meant to save it in Maya 2018.
@@ -0,0 +1,89 @@ | |||
#!/pxrpythonsubst | |||
# | |||
# Copyright 2016 Pixar |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New Autodesk copyright.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep, will update
|
||
@classmethod | ||
def setUpClass(cls): | ||
cls.inputPath = fixturesUtils .setUpClass(__file__) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra whitespace after fixtureUtils
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm...not sure how that happened.
Extracted the code to figure out the set of objects to export from the translator and commands into a single utility function.
Added code to filter out any objects that also have parent objects in the list. This was previously an error in the write job and would error out.