1 //-*****************************************************************************
2 //
3 // Copyright (c) 2009-2012,
4 //  Sony Pictures Imageworks Inc. and
5 //  Industrial Light & Magic, a division of Lucasfilm Entertainment Company Ltd.
6 //
7 // All rights reserved.
8 //
9 // Redistribution and use in source and binary forms, with or without
10 // modification, are permitted provided that the following conditions are
11 // met:
12 // *       Redistributions of source code must retain the above copyright
13 // notice, this list of conditions and the following disclaimer.
14 // *       Redistributions in binary form must reproduce the above
15 // copyright notice, this list of conditions and the following disclaimer
16 // in the documentation and/or other materials provided with the
17 // distribution.
18 // *       Neither the name of Sony Pictures Imageworks, nor
19 // Industrial Light & Magic, nor the names of their contributors may be used
20 // to endorse or promote products derived from this software without specific
21 // prior written permission.
22 //
23 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
24 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
25 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
26 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
27 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
28 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
29 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
30 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
31 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
32 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34 //
35 //-*****************************************************************************
36 
37 #include "MayaUtility.h"
38 
39 // this struct is used in function "bool util::isAnimated(MObject & object, bool checkParent)"
40 struct NodesToCheckStruct
41 {
42     MObject node;
43     bool    checkParent;
44 };
45 
46 // return seconds per frame
spf()47 double util::spf()
48 {
49     static const MTime sec(1.0, MTime::kSeconds);
50     return 1.0 / sec.as(MTime::uiUnit());
51 }
52 
isAncestorDescendentRelationship(const MDagPath & path1,const MDagPath & path2)53 bool util::isAncestorDescendentRelationship(const MDagPath & path1,
54     const MDagPath & path2)
55 {
56     unsigned int length1 = path1.length();
57     unsigned int length2 = path2.length();
58     unsigned int diff;
59 
60     if (length1 == length2 && !(path1 == path2))
61         return false;
62 
63     MDagPath ancestor, descendent;
64     if (length1 > length2)
65     {
66         ancestor = path2;
67         descendent = path1;
68         diff = length1 - length2;
69     }
70     else
71     {
72         ancestor = path1;
73         descendent = path2;
74         diff = length2 - length1;
75     }
76 
77     descendent.pop(diff);
78 
79     bool ret = (ancestor == descendent);
80 
81     if (ret)
82     {
83         MString err = path1.fullPathName() + " and ";
84         err += path2.fullPathName() + " have parenting relationships";
85         MGlobal::displayError(err);
86     }
87     return ret;
88 }
89 
90 
91 // returns 0 if static, 1 if sampled, and 2 if a curve
getSampledType(const MPlug & iPlug)92 int util::getSampledType(const MPlug& iPlug)
93 {
94     MPlugArray conns;
95 
96     iPlug.connectedTo(conns, true, false);
97 
98     // it's possible that only some element of an array plug or
99     // some component of a compound plus is connected
100     if (conns.length() == 0)
101     {
102         if (iPlug.isArray())
103         {
104             unsigned int numConnectedElements = iPlug.numConnectedElements();
105             for (unsigned int e = 0; e < numConnectedElements; e++)
106             {
107                 int retVal = getSampledType(iPlug.connectionByPhysicalIndex(e));
108                 if (retVal > 0)
109                     return retVal;
110             }
111         }
112         else if (iPlug.isCompound() && iPlug.numConnectedChildren() > 0)
113         {
114             unsigned int numChildren = iPlug.numChildren();
115             for (unsigned int c = 0; c < numChildren; c++)
116             {
117                 int retVal = getSampledType(iPlug.child(c));
118                 if (retVal > 0)
119                     return retVal;
120             }
121         }
122         return 0;
123     }
124 
125     MObject ob;
126     MFnDependencyNode nodeFn;
127     for (unsigned i = 0; i < conns.length(); i++)
128     {
129         ob = conns[i].node();
130         MFn::Type type = ob.apiType();
131 
132         switch (type)
133         {
134             case MFn::kAnimCurveTimeToAngular:
135             case MFn::kAnimCurveTimeToDistance:
136             case MFn::kAnimCurveTimeToTime:
137             case MFn::kAnimCurveTimeToUnitless:
138             {
139                 nodeFn.setObject(ob);
140                 MPlug incoming = nodeFn.findPlug("i", true);
141 
142                 // sampled
143                 if (incoming.isConnected())
144                     return 1;
145 
146                 // curve
147                 else
148                     return 2;
149             }
150             break;
151 
152             case MFn::kMute:
153             {
154                 nodeFn.setObject(ob);
155                 MPlug mutePlug = nodeFn.findPlug("mute", true);
156 
157                 // static
158                 if (mutePlug.asBool())
159                     return 0;
160                 // curve
161                 else
162                    return 2;
163             }
164             break;
165 
166             default:
167             break;
168         }
169     }
170 
171     return 1;
172 }
173 
getRotOrder(MTransformationMatrix::RotationOrder iOrder,unsigned int & oXAxis,unsigned int & oYAxis,unsigned int & oZAxis)174 bool util::getRotOrder(MTransformationMatrix::RotationOrder iOrder,
175     unsigned int & oXAxis, unsigned int & oYAxis, unsigned int & oZAxis)
176 {
177     switch (iOrder)
178     {
179         case MTransformationMatrix::kXYZ:
180         {
181             oXAxis = 0;
182             oYAxis = 1;
183             oZAxis = 2;
184         }
185         break;
186 
187         case MTransformationMatrix::kYZX:
188         {
189             oXAxis = 1;
190             oYAxis = 2;
191             oZAxis = 0;
192         }
193         break;
194 
195         case MTransformationMatrix::kZXY:
196         {
197             oXAxis = 2;
198             oYAxis = 0;
199             oZAxis = 1;
200         }
201         break;
202 
203         case MTransformationMatrix::kXZY:
204         {
205             oXAxis = 0;
206             oYAxis = 2;
207             oZAxis = 1;
208         }
209         break;
210 
211         case MTransformationMatrix::kYXZ:
212         {
213             oXAxis = 1;
214             oYAxis = 0;
215             oZAxis = 2;
216         }
217         break;
218 
219         case MTransformationMatrix::kZYX:
220         {
221             oXAxis = 2;
222             oYAxis = 1;
223             oZAxis = 0;
224         }
225         break;
226 
227         default:
228         {
229             return false;
230         }
231     }
232     return true;
233 }
234 
235 // 0 dont write, 1 write static 0, 2 write anim 0, 3 write anim -1
getVisibilityType(const MPlug & iPlug)236 int util::getVisibilityType(const MPlug & iPlug)
237 {
238     int type = getSampledType(iPlug);
239 
240     // static case
241     if (type == 0)
242     {
243         // dont write anything
244         if (iPlug.asBool())
245             return 0;
246 
247         // write static 0
248         return 1;
249     }
250     else
251     {
252         // anim write -1
253         if (iPlug.asBool())
254             return 3;
255 
256         // write anim 0
257         return 2;
258     }
259 }
260 
261 // does this cover all cases?
isAnimated(MObject & object,bool checkParent)262 bool util::isAnimated(MObject & object, bool checkParent)
263 {
264     MStatus stat;
265     MItDependencyGraph iter(object, MFn::kInvalid,
266         MItDependencyGraph::kUpstream,
267         MItDependencyGraph::kDepthFirst,
268         MItDependencyGraph::kPlugLevel,
269         &stat);
270 
271     if (stat!= MS::kSuccess)
272     {
273         MGlobal::displayError("Unable to create DG iterator ");
274     }
275 
276     // MAnimUtil::isAnimated(node) will search the history of the node
277     // for any animation curve nodes. It will return true for those nodes
278     // that have animation curve in their history.
279     // The average time complexity is O(n^2) where n is the number of history
280     // nodes. But we can improve the best case by split the loop into two.
281     std::vector<NodesToCheckStruct> nodesToCheckAnimCurve;
282 
283     NodesToCheckStruct nodeStruct;
284     for (; !iter.isDone(); iter.next())
285     {
286         MObject node = iter.currentItem();
287 
288         if (node.hasFn(MFn::kPluginDependNode) ||
289                 node.hasFn( MFn::kConstraint ) ||
290                 node.hasFn(MFn::kPointConstraint) ||
291                 node.hasFn(MFn::kAimConstraint) ||
292                 node.hasFn(MFn::kOrientConstraint) ||
293                 node.hasFn(MFn::kScaleConstraint) ||
294                 node.hasFn(MFn::kGeometryConstraint) ||
295                 node.hasFn(MFn::kNormalConstraint) ||
296                 node.hasFn(MFn::kTangentConstraint) ||
297                 node.hasFn(MFn::kParentConstraint) ||
298                 node.hasFn(MFn::kPoleVectorConstraint) ||
299                 node.hasFn(MFn::kParentConstraint) ||
300                 node.hasFn(MFn::kTime) ||
301                 node.hasFn(MFn::kJoint) ||
302                 node.hasFn(MFn::kGeometryFilt) ||
303                 node.hasFn(MFn::kTweak) ||
304                 node.hasFn(MFn::kPolyTweak) ||
305                 node.hasFn(MFn::kSubdTweak) ||
306                 node.hasFn(MFn::kCluster) ||
307                 node.hasFn(MFn::kFluid) ||
308                 node.hasFn(MFn::kPolyBoolOp))
309         {
310             return true;
311         }
312 
313         if (node.hasFn(MFn::kExpression))
314         {
315             MFnExpression fn(node, &stat);
316             if (stat == MS::kSuccess && fn.isAnimated())
317             {
318                 return true;
319             }
320         }
321 
322         // skip shading nodes
323         if (!node.hasFn(MFn::kShadingEngine))
324         {
325             MPlug plug = iter.thisPlug();
326             MFnAttribute attr(plug.attribute(), &stat);
327             bool checkNodeParent = false;
328             if (stat == MS::kSuccess && attr.isWorldSpace())
329             {
330                 checkNodeParent = true;
331             }
332 
333             nodeStruct.node = node;
334             nodeStruct.checkParent = checkParent || checkNodeParent;
335             nodesToCheckAnimCurve.push_back(nodeStruct);
336         }
337         else
338         {
339             // and don't traverse the rest of their subgraph
340             iter.prune();
341         }
342     }
343 
344     for (size_t i = 0; i < nodesToCheckAnimCurve.size(); i++)
345     {
346         if (MAnimUtil::isAnimated(nodesToCheckAnimCurve[i].node, nodesToCheckAnimCurve[i].checkParent))
347         {
348             return true;
349         }
350     }
351 
352     return false;
353 }
354 
isDrivenByFBIK(const MFnIkJoint & iJoint)355 bool util::isDrivenByFBIK(const MFnIkJoint & iJoint)
356 {
357     // check joints that are driven by Maya FBIK
358     // Maya FBIK has no connection to joints' TRS plugs
359     // but TRS of joints are driven by FBIK, they are not static
360     // Maya 2012's new HumanIK has connections to joints.
361     // FBIK is a special case.
362     MStatus status = MS::kSuccess;
363     if (iJoint.hikJointName(&status).length() > 0 && status) {
364         return true;
365     }
366     return false;
367 }
368 
isDrivenBySplineIK(const MFnIkJoint & iJoint)369 bool util::isDrivenBySplineIK(const MFnIkJoint & iJoint)
370 {
371     // spline IK can drive the starting joint's translate channel but
372     // it has no connection to the translate plug.
373     // we treat the joint as animated in this case.
374     // find the ikHandle node.
375     MPlug msgPlug = iJoint.findPlug("message", false);
376     MPlugArray msgPlugDst;
377     msgPlug.connectedTo(msgPlugDst, false, true);
378     for (unsigned int i = 0; i < msgPlugDst.length(); i++) {
379         MFnDependencyNode ikHandle(msgPlugDst[i].node());
380         if (!ikHandle.object().hasFn(MFn::kIkHandle)) continue;
381 
382         // find the ikSolver node.
383         MPlug ikSolverPlug = ikHandle.findPlug("ikSolver", true);
384         MPlugArray ikSolverDst;
385         ikSolverPlug.connectedTo(ikSolverDst, true, false);
386         for (unsigned int j = 0; j < ikSolverDst.length(); j++) {
387 
388             // return true if the ikSolver is a spline solver.
389             if (ikSolverDst[j].node().hasFn(MFn::kSplineSolver)) {
390                 return true;
391             }
392         }
393     }
394 
395     return false;
396 }
397 
isIntermediate(const MObject & object)398 bool util::isIntermediate(const MObject & object)
399 {
400     MStatus stat;
401     MFnDagNode mFn(object);
402 
403     MPlug plug = mFn.findPlug("intermediateObject", false, &stat);
404     if (stat == MS::kSuccess && plug.asBool())
405         return true;
406     else
407         return false;
408 }
409 
isRenderable(const MObject & object)410 bool util::isRenderable(const MObject & object)
411 {
412     MStatus stat;
413     MFnDagNode mFn(object);
414 
415     // templated turned on?  return false
416     MPlug plug = mFn.findPlug("template", false, &stat);
417     if (stat == MS::kSuccess && plug.asBool())
418         return false;
419 
420     // visibility or lodVisibility off?  return false
421     plug = mFn.findPlug("visibility", false, &stat);
422     if (stat == MS::kSuccess && !plug.asBool())
423     {
424         // the value is off. let's check if it has any in-connection,
425         // otherwise, it means it is not animated.
426         MPlugArray arrayIn;
427         plug.connectedTo(arrayIn, true, false, &stat);
428 
429         if (stat == MS::kSuccess && arrayIn.length() == 0)
430         {
431             return false;
432         }
433     }
434 
435     plug = mFn.findPlug("lodVisibility", false, &stat);
436     if (stat == MS::kSuccess && !plug.asBool())
437     {
438         MPlugArray arrayIn;
439         plug.connectedTo(arrayIn, true, false, &stat);
440 
441         if (stat == MS::kSuccess && arrayIn.length() == 0)
442         {
443             return false;
444         }
445     }
446 
447     // this shape is renderable
448     return true;
449 }
450 
stripNamespaces(const MString & iNodeName,unsigned int iDepth)451 MString util::stripNamespaces(const MString & iNodeName, unsigned int iDepth)
452 {
453     if (iDepth == 0)
454     {
455         return iNodeName;
456     }
457 
458     MStringArray strArray;
459     if (iNodeName.split(':', strArray) == MS::kSuccess)
460     {
461         unsigned int len = strArray.length();
462 
463         // we want to strip off more namespaces than what we have
464         // so we just return the last name
465         if (len == 0)
466         {
467             return iNodeName;
468         }
469         else if (len <= iDepth + 1)
470         {
471             return strArray[len-1];
472         }
473 
474         MString name;
475         for (unsigned int i = iDepth; i < len - 1; ++i)
476         {
477             name += strArray[i];
478             name += ":";
479         }
480         name += strArray[len-1];
481         return name;
482     }
483 
484     return iNodeName;
485 }
486 
getHelpText()487 MString util::getHelpText()
488 {
489     MString ret =
490 "AbcExport [options]\n"
491 "Options:\n"
492 "-h / -help  Print this message.\n"
493 "\n"
494 "-prs / -preRollStartFrame double\n"
495 "The frame to start scene evaluation at.  This is used to set the\n"
496 "starting frame for time dependent translations and can be used to evaluate\n"
497 "run-up that isn't actually translated.\n"
498 "\n"
499 "-duf / -dontSkipUnwrittenFrames\n"
500 "When evaluating multiple translate jobs, the presence of this flag decides\n"
501 "whether to evaluate frames between jobs when there is a gap in their frame\n"
502 "ranges.\n"
503 "\n"
504 "-v / -verbose\n"
505 "Prints the current frame that is being evaluated.\n"
506 "\n"
507 "-j / -jobArg string REQUIRED\n"
508 "String which contains flags for writing data to a particular file.\n"
509 "Multiple jobArgs can be specified.\n"
510 "\n"
511 "-jobArg flags:\n"
512 "\n"
513 "-a / -attr string\n"
514 "A specific geometric attribute to write out.\n"
515 "This flag may occur more than once.\n"
516 "\n"
517 "-as / -autoSubd\n"
518 "If this flag is present and the mesh has crease edges, crease vertices or holes, \n"
519 "the mesh (OPolyMesh) would now be written out as an OSubD and crease info will be stored in the Alembic \n"
520 "file. Otherwise, creases info won't be preserved in Alembic file \n"
521 "unless a custom Boolean attribute SubDivisionMesh has been added to mesh node and its value is true. \n"
522 "\n"
523 "-atp / -attrPrefix string (default ABC_)\n"
524 "Prefix filter for determining which geometric attributes to write out.\n"
525 "This flag may occur more than once.\n"
526 "\n"
527 "-df / -dataFormat string\n"
528 "The data format to use to write the file.  Can be either HDF or Ogawa.\n"
529 "The default is Ogawa.\n"
530 "\n"
531 "-ef / -eulerFilter\n"
532 "If this flag is present, apply Euler filter while sampling rotations.\n"
533 "\n"
534 "-f / -file string REQUIRED\n"
535 "File location to write the Alembic data.\n"
536 "\n"
537 "-fr / -frameRange double double\n"
538 "The frame range to write.\n"
539 "Multiple occurrences of -frameRange are supported within a job. Each\n"
540 "-frameRange defines a new frame range. -step or -frs will affect the\n"
541 "current frame range only.\n"
542 "\n"
543 "-frs / -frameRelativeSample double\n"
544 "frame relative sample that will be written out along the frame range.\n"
545 "This flag may occur more than once.\n"
546 "\n"
547 "-nn / -noNormals\n"
548 "If this flag is present normal data for Alembic poly meshes will not be\n"
549 "written.\n"
550 "\n"
551 "-pr / -preRoll\n"
552 "If this flag is present, this frame range will not be sampled.\n"
553 "\n"
554 "-ro / -renderableOnly\n"
555 "If this flag is present non-renderable hierarchy (invisible, or templated)\n"
556 "will not be written out.\n"
557 "\n"
558 "-rt / -root\n"
559 "Maya dag path which will be parented to the root of the Alembic file.\n"
560 "This flag may occur more than once.  If unspecified, it defaults to '|' which\n"
561 "means the entire scene will be written out.\n"
562 "\n"
563 "-s / -step double (default 1.0)\n"
564 "The time interval (expressed in frames) at which the frame range is sampled.\n"
565 "Additional samples around each frame can be specified with -frs.\n"
566 "\n"
567 "-sl / -selection\n"
568 "If this flag is present, write out all all selected nodes from the active\n"
569 "selection list that are descendents of the roots specified with -root.\n"
570 "\n"
571 "-sn / -stripNamespaces (optional int)\n"
572 "If this flag is present all namespaces will be stripped off of the node before\n"
573 "being written to Alembic.  If an optional int is specified after the flag\n"
574 "then that many namespaces will be stripped off of the node name. Be careful\n"
575 "that the new stripped name does not collide with other sibling node names.\n\n"
576 "Examples: \n"
577 "taco:foo:bar would be written as just bar with -sn\n"
578 "taco:foo:bar would be written as foo:bar with -sn 1\n"
579 "\n"
580 "-u / -userAttr string\n"
581 "A specific user attribute to write out.  This flag may occur more than once.\n"
582 "\n"
583 "-uatp / -userAttrPrefix string\n"
584 "Prefix filter for determining which user attributes to write out.\n"
585 "This flag may occur more than once.\n"
586 "\n"
587 "-uv / -uvWrite\n"
588 "If this flag is present, uv data for PolyMesh and SubD shapes will be written to\n"
589 "the Alembic file.  Only the current uv map is used.\n"
590 "\n"
591 "-uvo / -uvsOnly\n"
592 "If this flag is present, only uv data for PolyMesh and SubD shapes will be written\n"
593 "to the Alembic file.  Only the current uv map is used.\n"
594 "\n"
595 "-wcs / -writeColorSets\n"
596 "Write all color sets on MFnMeshes as color 3 or color 4 indexed geometry \n"
597 "parameters with face varying scope.\n"
598 "\n"
599 "-wfs / -writeFaceSets\n"
600 "Write all Face sets on MFnMeshes.\n"
601 "\n"
602 "-wfg / -wholeFrameGeo\n"
603 "If this flag is present data for geometry will only be written out on whole\n"
604 "frames.\n"
605 "\n"
606 "-ws / -worldSpace\n"
607 "If this flag is present, any root nodes will be stored in world space.\n"
608 "\n"
609 "-wv / -writeVisibility\n"
610 "If this flag is present, visibility state will be stored in the Alembic\n"
611 "file.  Otherwise everything written out is treated as visible.\n"
612 "\n"
613 "-wuvs / -writeUVSets\n"
614 "Write all uv sets on MFnMeshes as vector 2 indexed geometry \n"
615 "parameters with face varying scope.\n"
616 "\n"
617 "-mfc / -melPerFrameCallback string\n"
618 "When each frame (and the static frame) is evaluated the string specified is\n"
619 "evaluated as a Mel command. See below for special processing rules.\n"
620 "\n"
621 "-mpc / -melPostJobCallback string\n"
622 "When the translation has finished the string specified is evaluated as a Mel\n"
623 "command. See below for special processing rules.\n"
624 "\n"
625 "-pfc / -pythonPerFrameCallback string\n"
626 "When each frame (and the static frame) is evaluated the string specified is\n"
627 "evaluated as a python command. See below for special processing rules.\n"
628 "\n"
629 "-ppc / -pythonPostJobCallback string\n"
630 "When the translation has finished the string specified is evaluated as a\n"
631 "python command. See below for special processing rules.\n"
632 "\n"
633 "Special callback information:\n"
634 "On the callbacks, special tokens are replaced with other data, these tokens\n"
635 "and what they are replaced with are as follows:\n"
636 "\n"
637 "#FRAME# replaced with the frame number being evaluated.\n"
638 "#FRAME# is ignored in the post callbacks.\n"
639 "\n"
640 "#BOUNDS# replaced with a string holding bounding box values in minX minY minZ\n"
641 "maxX maxY maxZ space seperated order.\n"
642 "\n"
643 "#BOUNDSARRAY# replaced with the bounding box values as above, but in\n"
644 "array form.\n"
645 "In Mel: {minX, minY, minZ, maxX, maxY, maxZ}\n"
646 "In Python: [minX, minY, minZ, maxX, maxY, maxZ]\n"
647 "\n"
648 "Examples:\n"
649 "\n"
650 "AbcExport -j \"-root |group|foo -root |test|path|bar -file /tmp/test.abc\"\n"
651 "Writes out everything at foo and below and bar and below to /tmp/test.abc.\n"
652 "foo and bar are siblings parented to the root of the Alembic scene.\n"
653 "\n"
654 "AbcExport -j \"-frameRange 1 5 -step 0.5 -root |group|foo -file /tmp/test.abc\"\n"
655 "Writes out everything at foo and below to /tmp/test.abc sampling at frames:\n"
656 "1 1.5 2 2.5 3 3.5 4 4.5 5\n"
657 "\n"
658 "AbcExport -j \"-fr 0 10 -frs -0.1 -frs 0.2 -step 5 -file /tmp/test.abc\"\n"
659 "Writes out everything in the scene to /tmp/test.abc sampling at frames:\n"
660 "-0.1 0.2 4.9 5.2 9.9 10.2\n"
661 "\n"
662 "Note: The difference between your highest and lowest frameRelativeSample can\n"
663 "not be greater than your step size.\n"
664 "\n"
665 "AbcExport -j \"-step 0.25 -frs 0.3 -frs 0.60 -fr 1 5 -root foo -file test.abc\"\n"
666 "\n"
667 "Is illegal because the highest and lowest frameRelativeSamples are 0.3 frames\n"
668 "apart.\n"
669 "\n"
670 "AbcExport -j \"-sl -root |group|foo -file /tmp/test.abc\"\n"
671 "Writes out all selected nodes and it's ancestor nodes including up to foo.\n"
672 "foo will be parented to the root of the Alembic scene.\n"
673 "\n";
674 
675     return ret;
676 }
677