diff --git a/plugin/al/lib/AL_USDMaya/AL/usdmaya/fileio/Export.cpp b/plugin/al/lib/AL_USDMaya/AL/usdmaya/fileio/Export.cpp index 6c5948a8fa..ef374542a6 100644 --- a/plugin/al/lib/AL_USDMaya/AL/usdmaya/fileio/Export.cpp +++ b/plugin/al/lib/AL_USDMaya/AL/usdmaya/fileio/Export.cpp @@ -282,6 +282,49 @@ struct Export::Impl } } + void addSelection(MDagPath parentDagPath, const std::string& path) + { + const std::string parent(parentDagPath.fullPathName().asChar()); + m_selectionMap[parent].emplace(path); + // Add all of the parent paths + m_parentsMap.emplace(parent); + while (parentDagPath.pop() == MStatus::kSuccess && parentDagPath.isValid()) { + std::string pathStr(parentDagPath.fullPathName().asChar()); + if (m_parentsMap.find(pathStr) != m_parentsMap.end()) { + // Parent path has been added + return; + } + m_parentsMap.emplace(pathStr); + } + } + + bool isPathExcluded(MDagPath dagPath) const + { + // Excluded path is: + // 1) path itself is not selected; AND + // 2) none of any ascendants or descendants are selected + + const std::string pathStr(dagPath.fullPathName().asChar()); + if (dagPath.pop() != MStatus::kSuccess || !dagPath.isValid()) { + // Path is likely the root level node + return false; + } + // Check if the target path itself is selected or not + auto it = m_selectionMap.find(dagPath.fullPathName().asChar()); + if (it != m_selectionMap.cend()) { + // Found parent path in cache map, either target path or any of its siblings + // is selected + if (it->second.find(pathStr) == it->second.cend()) { + // One of the target path's sibling is selected, do further checking + // to see if any of the children is select or not. + // If not found, this target path should be excluded. + return m_parentsMap.find(pathStr) == m_parentsMap.cend(); + } + // Reaching here means the target path itself is selected + } + return false; + } + private: #if AL_UTILS_ENABLE_SIMD std::map m_nodeMap; @@ -290,8 +333,10 @@ struct Export::Impl std::map m_nodeMap; std::map m_instanceMap; #endif - UsdStageRefPtr m_stage; - UsdPrim m_instancesPrim; + std::unordered_map> m_selectionMap; + std::unordered_set m_parentsMap; + UsdStageRefPtr m_stage; + UsdPrim m_instancesPrim; }; static MObject g_transform_rotateAttr = MObject::kNullObj; @@ -509,11 +554,11 @@ void Export::exportIkChain(MDagPath effectorPath, const SdfPath& usdPath) } //---------------------------------------------------------------------------------------------------------------------- -void Export::copyTransformParams(UsdPrim prim, MFnTransform& fnTransform) +void Export::copyTransformParams(UsdPrim prim, MFnTransform& fnTransform, bool exportInWorldSpace) { translators::TransformTranslator::copyAttributes( - fnTransform.object(), prim, m_params, fnTransform.dagPath()); + fnTransform.object(), prim, m_params, fnTransform.dagPath(), exportInWorldSpace); if (m_params.m_dynamicAttributes) { translators::DgNodeTranslator::copyDynamicAttributes( fnTransform.object(), prim, m_params.m_animTranslator); @@ -564,7 +609,8 @@ void Export::addReferences( MFnTransform& fnTransform, SdfPath& usdPath, const SdfPath& instancePath, - ReferenceType refType) + ReferenceType refType, + bool exportInWorldSpace) { UsdStageRefPtr stage = m_impl->stage(); if (refType == kShapeReference) { @@ -583,7 +629,7 @@ void Export::addReferences( } break; case kShapeReference: { - copyTransformParams(usdPrim, fnTransform); + copyTransformParams(usdPrim, fnTransform, exportInWorldSpace); } break; default: break; @@ -604,7 +650,8 @@ void Export::exportShapesCommonProc( MDagPath shapePath, MFnTransform& fnTransform, SdfPath& usdPath, - const ReferenceType refType) + const ReferenceType refType, + bool exportInWorldSpace) { UsdPrim transformPrim; @@ -663,7 +710,7 @@ void Export::exportShapesCommonProc( } if (m_params.m_mergeTransforms && copyTransform) { - copyTransformParams(transformPrim, fnTransform); + copyTransformParams(transformPrim, fnTransform, exportInWorldSpace); } } @@ -687,13 +734,14 @@ void Export::exportSceneHierarchy(MDagPath rootPath, SdfPath& defaultPrim) MItDag it(MItDag::kDepthFirst); it.reset(rootPath, MItDag::kDepthFirst, MFn::kTransform); - std::function exportShapeProc + std::function exportShapeProc = [this]( MDagPath shapePath, MFnTransform& fnTransform, SdfPath& usdPath, - ReferenceType refType) { - this->exportShapesCommonProc(shapePath, fnTransform, usdPath, refType); + ReferenceType refType, + bool inWorldSpace) { + this->exportShapesCommonProc(shapePath, fnTransform, usdPath, refType, inWorldSpace); }; std::function exportTransformFunc @@ -717,13 +765,13 @@ void Export::exportSceneHierarchy(MDagPath rootPath, SdfPath& defaultPrim) if (translatorPtr) { UsdPrim transformPrim = translatorPtr->exportObject(m_impl->stage(), transformPath, path, m_params); - this->copyTransformParams(transformPrim, fnTransform); + this->copyTransformParams(transformPrim, fnTransform, inWorldSpace); exportKids = translatorPtr->exportDescendants(); } else { UsdGeomXform xform = UsdGeomXform::Define(m_impl->stage(), path); UsdPrim transformPrim = xform.GetPrim(); - this->copyTransformParams(transformPrim, fnTransform); + this->copyTransformParams(transformPrim, fnTransform, inWorldSpace); } return exportKids; }; @@ -734,7 +782,8 @@ void Export::exportSceneHierarchy(MDagPath rootPath, SdfPath& defaultPrim) MDagPath shapePath, MFnTransform& fnTransform, SdfPath& usdPath, - ReferenceType refType) { + ReferenceType refType, + bool inWorldSpace) { this->exportShapesOnlyUVProc(shapePath, fnTransform, usdPath); }; exportTransformFunc = [this]( @@ -751,17 +800,36 @@ void Export::exportSceneHierarchy(MDagPath rootPath, SdfPath& defaultPrim) } MFnTransform fnTransform; + bool exportInWorldSpace = m_params.m_exportInWorldSpace; // loop through transforms only while (!it.isDone()) { // assign transform function set MDagPath transformPath; it.getPath(transformPath); + // Check if any sibling is selected, exclude this node if not selected + if (m_impl->isPathExcluded(transformPath)) { + it.prune(); + it.next(); + continue; + } + + if (exportInWorldSpace && !(transformPath == rootPath)) { + exportInWorldSpace = false; + } + fnTransform.setObject(transformPath); // Make sure we haven't seen this transform before. bool transformHasBeenExported = m_impl->contains(fnTransform); if (transformHasBeenExported) { + // "rootPath" is the user selected node but if it has been exported, + // that means user selected a sub node under another selected node. + if (transformPath == rootPath) { + it.prune(); + it.next(); + continue; + } // We have an instanced shape! std::cout << "encountered transform instance " << fnTransform.fullPathName().asChar() << std::endl; @@ -794,9 +862,9 @@ void Export::exportSceneHierarchy(MDagPath rootPath, SdfPath& defaultPrim) uint32_t numShapes; transformPath.numberOfShapesDirectlyBelow(numShapes); - if (!m_params.m_mergeTransforms && !m_params.m_exportInWorldSpace) { - bool exportKids = exportTransformFunc( - transformPath, fnTransform, usdPath, m_params.m_exportInWorldSpace); + if (!m_params.m_mergeTransforms && !exportInWorldSpace) { + bool exportKids + = exportTransformFunc(transformPath, fnTransform, usdPath, exportInWorldSpace); UsdPrim prim = m_impl->stage()->GetPrimAtPath(usdPath); prim.SetMetadata( AL::usdmaya::Metadata::mergedTransform, AL::usdmaya::Metadata::unmerged); @@ -832,7 +900,8 @@ void Export::exportSceneHierarchy(MDagPath rootPath, SdfPath& defaultPrim) refType = m_params.m_mergeTransforms ? kShapeReference : kTransformReference; } - exportShapeProc(shapePath, fnTransform, shapeUsdPath, refType); + exportShapeProc( + shapePath, fnTransform, shapeUsdPath, refType, exportInWorldSpace); } else { refType = m_params.m_mergeTransforms ? kShapeReference : kTransformReference; @@ -840,17 +909,29 @@ void Export::exportSceneHierarchy(MDagPath rootPath, SdfPath& defaultPrim) if (refType == kShapeReference) { SdfPath instancePath = m_impl->getMasterPath(shapeDag); - addReferences(shapePath, fnTransform, shapeUsdPath, instancePath, refType); + addReferences( + shapePath, + fnTransform, + shapeUsdPath, + instancePath, + refType, + exportInWorldSpace); } else if (refType == kTransformReference) { SdfPath instancePath = m_impl->getMasterPath(MFnDagNode(shapeDag.parent(0))); - addReferences(shapePath, fnTransform, usdPath, instancePath, refType); + addReferences( + shapePath, + fnTransform, + usdPath, + instancePath, + refType, + exportInWorldSpace); } } } else { if (m_params.m_mergeTransforms) { if (!exportTransformFunc( - transformPath, fnTransform, usdPath, m_params.m_exportInWorldSpace)) { + transformPath, fnTransform, usdPath, exportInWorldSpace)) { it.prune(); } } @@ -884,26 +965,36 @@ void Export::doExport() m_impl->createInstancesPrim(); } - MObjectArray objects; const MSelectionList& sl = m_params.m_nodes; SdfPath defaultPrim; + // Use std::map to keep the same order regardless of the selection order from Maya + // This makes sure we process the objects from top to bottom + std::map selectedPaths; for (uint32_t i = 0, n = sl.length(); i < n; ++i) { MDagPath path; if (sl.getDagPath(i, path)) { + std::string pathStr; if (path.node().hasFn(MFn::kTransform)) { - exportSceneHierarchy(path, defaultPrim); + pathStr = path.fullPathName().asChar(); } else if (path.node().hasFn(MFn::kShape)) { path.pop(); - exportSceneHierarchy(path, defaultPrim); + pathStr = path.fullPathName().asChar(); + } else { + continue; + } + selectedPaths.emplace(pathStr, path); + // Store direct parent path + if (path.pop() == MStatus::kSuccess && path.isValid()) { + m_impl->addSelection(path, pathStr); } - } else { - MObject obj; - sl.getDependNode(i, obj); - objects.append(obj); } } + for (const auto& pathPair : selectedPaths) { + exportSceneHierarchy(pathPair.second, defaultPrim); + } + if (m_params.m_animTranslator) { m_params.m_animTranslator->exportAnimation(m_params); m_impl->setAnimationFrame(m_params.m_minFrame, m_params.m_maxFrame); diff --git a/plugin/al/lib/AL_USDMaya/AL/usdmaya/fileio/Export.h b/plugin/al/lib/AL_USDMaya/AL/usdmaya/fileio/Export.h index 27b39acfdd..7c7050b452 100644 --- a/plugin/al/lib/AL_USDMaya/AL/usdmaya/fileio/Export.h +++ b/plugin/al/lib/AL_USDMaya/AL/usdmaya/fileio/Export.h @@ -70,7 +70,8 @@ class Export MDagPath shapePath, MFnTransform& fnTransform, SdfPath& usdPath, - ReferenceType refType); + ReferenceType refType, + bool exportInWorldSpace); void exportShapesOnlyUVProc(MDagPath shapePath, MFnTransform& fnTransform, SdfPath& usdPath); UsdPrim exportMeshUV(MDagPath path, const SdfPath& usdPath); UsdPrim exportAssembly(MDagPath path, const SdfPath& usdPath); @@ -78,14 +79,15 @@ class Export UsdPrim exportPluginShape(MDagPath path, const SdfPath& usdPath); void exportIkChain(MDagPath effectorPath, const SdfPath& usdPath); void exportGeometryConstraint(MDagPath effectorPath, const SdfPath& usdPath); - void copyTransformParams(UsdPrim prim, MFnTransform& fnTransform); + void copyTransformParams(UsdPrim prim, MFnTransform& fnTransform, bool exportInWorldSpace); SdfPath determineUsdPath(MDagPath path, const SdfPath& usdPath, ReferenceType refType); void addReferences( MDagPath shapePath, MFnTransform& fnTransform, SdfPath& usdPath, const SdfPath& instancePath, - ReferenceType refType); + ReferenceType refType, + bool exportInWorldSpace); inline bool isPrimDefined(SdfPath& usdPath); struct Impl; void doExport(); diff --git a/plugin/al/lib/AL_USDMaya/AL/usdmaya/fileio/ExportParams.h b/plugin/al/lib/AL_USDMaya/AL/usdmaya/fileio/ExportParams.h index 69b254f7d7..27f25d1f64 100644 --- a/plugin/al/lib/AL_USDMaya/AL/usdmaya/fileio/ExportParams.h +++ b/plugin/al/lib/AL_USDMaya/AL/usdmaya/fileio/ExportParams.h @@ -56,9 +56,8 @@ struct ExporterParams bool m_animation = false; ///< if true, animation will be exported. bool m_useTimelineRange = false; ///< if true, then the export uses Maya's timeline range. bool m_filterSample = false; ///< if true, duplicate sample of attribute will be filtered out - bool m_exportInWorldSpace - = false; ///< if true, transform hierarchies will be flattened to a single WS transform PRIM - ///< (and no parents will be written out) + bool m_exportInWorldSpace = false; ///< if true, transform will be baked at the root prim, + ///< children under the root will be untouched. AnimationTranslator* m_animTranslator = 0; ///< the animation translator to help exporting the animation data bool m_extensiveAnimationCheck diff --git a/plugin/al/lib/AL_USDMaya/AL/usdmaya/fileio/translators/TransformTranslator.cpp b/plugin/al/lib/AL_USDMaya/AL/usdmaya/fileio/translators/TransformTranslator.cpp index 5363fa9878..dfd5c20d08 100644 --- a/plugin/al/lib/AL_USDMaya/AL/usdmaya/fileio/translators/TransformTranslator.cpp +++ b/plugin/al/lib/AL_USDMaya/AL/usdmaya/fileio/translators/TransformTranslator.cpp @@ -661,7 +661,8 @@ MStatus TransformTranslator::copyAttributes( const MObject& from, UsdPrim& to, const ExporterParams& params, - const MDagPath& path) + const MDagPath& path, + bool exportInWorldSpace) { UsdGeomXform xformSchema(to); GfVec3f scale; @@ -698,7 +699,7 @@ MStatus TransformTranslator::copyAttributes( transformAnimated = animTranslator->isAnimatedTransform(from); } - if (!params.m_exportInWorldSpace) { + if (!exportInWorldSpace) { getBool(from, m_inheritsTransform, inheritsTransform); getBool(from, m_visible, visible); getVec3(from, m_scale, (float*)&scale); diff --git a/plugin/al/lib/AL_USDMaya/AL/usdmaya/fileio/translators/TransformTranslator.h b/plugin/al/lib/AL_USDMaya/AL/usdmaya/fileio/translators/TransformTranslator.h index f3b0f06d3e..f179e193ea 100644 --- a/plugin/al/lib/AL_USDMaya/AL/usdmaya/fileio/translators/TransformTranslator.h +++ b/plugin/al/lib/AL_USDMaya/AL/usdmaya/fileio/translators/TransformTranslator.h @@ -69,13 +69,15 @@ class TransformTranslator : public DagNodeTranslator /// \param to the USD prim to copy the attributes to /// \param params the exporter params to determine what should be exported /// \param path the dag path + /// \param exportInWorldSpace parameter to determine if world space xform should be exported /// \return MS::kSuccess if ok AL_USDMAYA_PUBLIC static MStatus copyAttributes( const MObject& from, UsdPrim& to, const ExporterParams& params, - const MDagPath& path); + const MDagPath& path, + bool exportInWorldSpace); /// \brief copy the attribute value from the plug specified, at the given time, and store the /// data on the usdAttr. \param attr the attribute to be copied \param usdAttr the attribute diff --git a/plugin/al/plugin/AL_USDMayaTestPlugin/test_translators_TransformTranslator.cpp b/plugin/al/plugin/AL_USDMayaTestPlugin/test_translators_TransformTranslator.cpp index 315de94ebd..5743c5a03a 100644 --- a/plugin/al/plugin/AL_USDMayaTestPlugin/test_translators_TransformTranslator.cpp +++ b/plugin/al/plugin/AL_USDMayaTestPlugin/test_translators_TransformTranslator.cpp @@ -70,7 +70,8 @@ TEST(translators_TranformTranslator, io) EXPECT_EQ( MStatus(MS::kSuccess), - TransformTranslator::copyAttributes(node, prim, eparams, fn.dagPath())); + TransformTranslator::copyAttributes( + node, prim, eparams, fn.dagPath(), eparams.m_exportInWorldSpace)); MObject nodeB = xlator.createNode(prim, MObject::kNullObj, "transform", iparams); @@ -139,7 +140,8 @@ TEST(translators_TranformTranslator, animated_io) EXPECT_EQ( MStatus(MS::kSuccess), - TransformTranslator::copyAttributes(node, prim, eparams, fn.dagPath())); + TransformTranslator::copyAttributes( + node, prim, eparams, fn.dagPath(), eparams.m_exportInWorldSpace)); eparams.m_animTranslator->exportAnimation(eparams); ////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -235,3 +237,192 @@ TEST(translators_TranformTranslator, worldSpaceExport) EXPECT_NEAR(2.0, transform[3][1], 1e-6); EXPECT_NEAR(3.0, transform[3][2], 1e-6); } + +TEST(translators_TranformTranslator, worldSpaceGroupsExport) +{ + MFileIO::newFile(true); + + // clang-format off + // Command below creates a hierarchy like this: + // Maya Selection Expected in USD + // A * A + // X1 * X1 + // Y1 * Y1 # Only Y1 exists + // Y2 # Siblings should be excluded + // Y3 + // X2 # X2 is excluded because X1 is selected + // Y1 + // Y2 + // Y3 + // X3 X3 # root node A and child Y3 is selected, X3 is preserved + // Y1 # Y1 is excluded because of Y3 + // Y2 # Y2 is excluded because of Y3 + // Y3 * Y3 + // B # B is excluded + // X1 # root node B is not selected, X1 is excluded + // Y1 * Y1 # none of Y1's parents are selected, Y1 becomes a root prim in USD + // Y2 * Y2 # none of Y2's parents are selected, Y2 becomes a root prim in USD + // Y3 + // X2 + // Y1 + // cube1 + // cube1Shape * cube1 # none of Y2's parents are selected, the cube becomes a root prim in USD + // Y2 + // Y3 + // X3 + // C * C # C is exported as it is + // X1 X1 # X1 is exported as it is + // X2 X2 # X2 is exported as it is + // clang-format on + + MString buildCommand = + R"( + polyCube -n "cube1"; + group -n "Y1" "cube1"; move 1 1 1; duplicate -n "Y2" "Y1"; duplicate -n "Y3" "Y1"; + group -n "X1" "Y1" "Y2" "Y3"; move 1 1 1; duplicate -n "X2" "X1"; duplicate -n "X3" "X1"; + group -n "A" "X1" "X2" "X3"; move 1 1 1; duplicate -n "B" "A"; duplicate -n "C" "A"; + delete "|C|X3" "|C|X1|Y2" "|C|X1|Y3"; + select -r "|A" "|A|X1" "|A|X1|Y1" "|A|X3|Y3" "|B|X1|Y1" "|B|X1|Y2" "|B|X2|Y1|cube1|cube1Shape" "|C"; + )"; + ASSERT_TRUE(MGlobal::executeCommand(buildCommand)); + + auto path = buildTempPath("AL_USDMayaTests_exportInWorldSpaceMultipleGroups.usda"); + MString exportCommand = "file -force -options " + "\"Dynamic_Attributes=0;Meshes=1;Mesh_Face_Connects=1;Mesh_Points=1;" + "Mesh_Normals=0;Mesh_Vertex_Creases=0;" + "Mesh_Edge_Creases=0;Mesh_UVs=0;Mesh_UV_Only=0;Mesh_Points_as_PRef=0;" + "Mesh_Colours=0;Mesh_Holes=0;Compaction_Level=0;" + "Nurbs_Curves=0;Duplicate_Instances=0;Merge_Transforms=1;Animation=1;" + "Use_Timeline_Range=0;Frame_Min=1;" + "Frame_Max=2;Sub_Samples=1;Filter_Sample=0;Export_At_Which_Time=2;" + "Export_In_World_Space=1;\" -typ \"AL usdmaya export\" -pr -es "; + exportCommand += "\""; + exportCommand += path; + exportCommand += "\""; + + // Export cube in world space + ASSERT_TRUE(MGlobal::executeCommand(exportCommand)); + + auto stage = UsdStage::Open(path); + ASSERT_TRUE(stage); + + bool resetsXformStack = false; + GfMatrix4d transform; + + // === Test the A group + auto primA = stage->GetPrimAtPath(SdfPath("/A")); + ASSERT_TRUE(primA); + { + UsdGeomXform xform(primA); + xform.GetLocalTransformation(&transform, &resetsXformStack, UsdTimeCode::EarliestTime()); + // Make sure the local space tm values match the world coords. + EXPECT_NEAR(1.0, transform[3][0], 1e-6); + EXPECT_NEAR(1.0, transform[3][1], 1e-6); + EXPECT_NEAR(1.0, transform[3][2], 1e-6); + } + + // /A/X1: the child group should have no change + auto primA_X1 = stage->GetPrimAtPath(SdfPath("/A/X1")); + ASSERT_TRUE(primA_X1); + { + UsdGeomXform xform(primA_X1); + xform.GetLocalTransformation(&transform, &resetsXformStack, UsdTimeCode::EarliestTime()); + // The local space tm values should be untouched + EXPECT_NEAR(1.0, transform[3][0], 1e-6); + EXPECT_NEAR(1.0, transform[3][1], 1e-6); + EXPECT_NEAR(1.0, transform[3][2], 1e-6); + } + auto primA_X1_Y1 = stage->GetPrimAtPath(SdfPath("/A/X1/Y1")); + ASSERT_TRUE(primA_X1_Y1); + { + UsdGeomXform xform(primA_X1_Y1); + xform.GetLocalTransformation(&transform, &resetsXformStack, UsdTimeCode::EarliestTime()); + // The local space tm values should be untouched + EXPECT_NEAR(1.0, transform[3][0], 1e-6); + EXPECT_NEAR(1.0, transform[3][1], 1e-6); + EXPECT_NEAR(1.0, transform[3][2], 1e-6); + } + auto primA_X1_Y1_cube = stage->GetPrimAtPath(SdfPath("/A/X1/Y1/cube1")); + ASSERT_TRUE(primA_X1_Y1_cube); + { + UsdGeomXform xform(primA_X1_Y1_cube); + xform.GetLocalTransformation(&transform, &resetsXformStack, UsdTimeCode::EarliestTime()); + // The local space tm values should be untouched + EXPECT_NEAR(0.0, transform[3][0], 1e-6); + EXPECT_NEAR(0.0, transform[3][1], 1e-6); + EXPECT_NEAR(0.0, transform[3][2], 1e-6); + } + + // Test the rest of the prims in hierarchy + // /A/X2 should not be there + ASSERT_FALSE(stage->GetPrimAtPath(SdfPath("/A/X2"))); + // /A/X3/Y3/cube1 should be preserved + ASSERT_TRUE(stage->GetPrimAtPath(SdfPath("/A/X3/Y3/cube1"))); + + // === Test the B group + // /B should not be there + ASSERT_FALSE(stage->GetPrimAtPath(SdfPath("/B"))); + + // The child Y1 in B should now become a root level prim with world space xform baked + auto primB_Y1 = stage->GetPrimAtPath(SdfPath("/Y1")); + ASSERT_TRUE(primB_Y1); + { + UsdGeomXform xform(primB_Y1); + xform.GetLocalTransformation(&transform, &resetsXformStack, UsdTimeCode::EarliestTime()); + // The local space tm values should be untouched + EXPECT_NEAR(3.0, transform[3][0], 1e-6); + EXPECT_NEAR(3.0, transform[3][1], 1e-6); + EXPECT_NEAR(3.0, transform[3][2], 1e-6); + } + + // The nested cube under Y1 should be untouched + auto primB_Y1_cube = stage->GetPrimAtPath(SdfPath("/Y1/cube1")); + ASSERT_TRUE(primB_Y1_cube); + { + UsdGeomXform xform(primB_Y1_cube); + xform.GetLocalTransformation(&transform, &resetsXformStack, UsdTimeCode::EarliestTime()); + // The local space tm values should be untouched + EXPECT_NEAR(0.0, transform[3][0], 1e-6); + EXPECT_NEAR(0.0, transform[3][1], 1e-6); + EXPECT_NEAR(0.0, transform[3][2], 1e-6); + } + + // Same for Y2 in B + auto primB_Y2 = stage->GetPrimAtPath(SdfPath("/Y2")); + ASSERT_TRUE(primB_Y2); + { + UsdGeomXform xform(primB_Y2); + xform.GetLocalTransformation(&transform, &resetsXformStack, UsdTimeCode::EarliestTime()); + // The local space tm values should be untouched + EXPECT_NEAR(3.0, transform[3][0], 1e-6); + EXPECT_NEAR(3.0, transform[3][1], 1e-6); + EXPECT_NEAR(3.0, transform[3][2], 1e-6); + } + auto primB_Y2_cube = stage->GetPrimAtPath(SdfPath("/Y2/cube1")); + ASSERT_TRUE(primB_Y2_cube); + + // leaf cube in B also becomes a root level prim + auto primB_cube = stage->GetPrimAtPath(SdfPath("/cube1")); + ASSERT_TRUE(primB_cube); + { + UsdGeomXform xform(primB_cube); + xform.GetLocalTransformation(&transform, &resetsXformStack, UsdTimeCode::EarliestTime()); + // The local space tm values should be untouched + EXPECT_NEAR(3.0, transform[3][0], 1e-6); + EXPECT_NEAR(3.0, transform[3][1], 1e-6); + EXPECT_NEAR(3.0, transform[3][2], 1e-6); + } + + // === Test the C group hierarchy + ASSERT_TRUE(stage->GetPrimAtPath(SdfPath("/C"))); + ASSERT_TRUE(stage->GetPrimAtPath(SdfPath("/C/X1"))); + ASSERT_TRUE(stage->GetPrimAtPath(SdfPath("/C/X1/Y1"))); + ASSERT_TRUE(stage->GetPrimAtPath(SdfPath("/C/X1/Y1/cube1"))); + ASSERT_TRUE(stage->GetPrimAtPath(SdfPath("/C/X2"))); + ASSERT_TRUE(stage->GetPrimAtPath(SdfPath("/C/X2/Y1"))); + ASSERT_TRUE(stage->GetPrimAtPath(SdfPath("/C/X2/Y1/cube1"))); + ASSERT_TRUE(stage->GetPrimAtPath(SdfPath("/C/X2/Y2"))); + ASSERT_TRUE(stage->GetPrimAtPath(SdfPath("/C/X2/Y2/cube1"))); + ASSERT_TRUE(stage->GetPrimAtPath(SdfPath("/C/X2/Y3"))); + ASSERT_TRUE(stage->GetPrimAtPath(SdfPath("/C/X2/Y3/cube1"))); +}