From 3a27338980f767626810762e9cc39857dc5513fb Mon Sep 17 00:00:00 2001 From: Pierre Baillargeon Date: Fri, 10 Mar 2023 15:17:29 -0500 Subject: [PATCH 1/3] MAYA-128211 block command when layers are muted When there is at least one muted layers, block the delete, rename, group and reparent commands. These are commands that re-organize the hierarchy and must thus affect all layers with opinions. When some of these layers are muted, they would not be able to be updated with the new prim path, thus we prevent the operation. Add unit tests for the commands. Note that reparent and group are composite commands using the same underlying command (insert child) and thus require only one test. --- lib/mayaUsd/ufe/UsdUndoDeleteCommand.cpp | 33 +---------- lib/mayaUsd/ufe/UsdUndoInsertChildCommand.cpp | 2 + lib/mayaUsd/ufe/UsdUndoRenameCommand.cpp | 2 + lib/mayaUsd/utils/layers.cpp | 31 ++++++++++ lib/mayaUsd/utils/layers.h | 20 +++++++ test/lib/ufe/testDeleteCmd.py | 53 +++++++++++++++-- test/lib/ufe/testGroupCmd.py | 59 +++++++++++++++++++ test/lib/ufe/testRename.py | 58 ++++++++++++++++++ 8 files changed, 221 insertions(+), 37 deletions(-) diff --git a/lib/mayaUsd/ufe/UsdUndoDeleteCommand.cpp b/lib/mayaUsd/ufe/UsdUndoDeleteCommand.cpp index 9ff736f2bb..fdfbeaab12 100644 --- a/lib/mayaUsd/ufe/UsdUndoDeleteCommand.cpp +++ b/lib/mayaUsd/ufe/UsdUndoDeleteCommand.cpp @@ -21,7 +21,6 @@ #include #include -#include #include #include @@ -33,29 +32,6 @@ #include #endif -namespace { -#ifdef MAYA_ENABLE_NEW_PRIM_DELETE -bool hasLayersMuted(const PXR_NS::UsdPrim& prim) -{ - const PXR_NS::PcpPrimIndex& primIndex = prim.GetPrimIndex(); - - for (const PXR_NS::PcpNodeRef node : primIndex.GetNodeRange()) { - - TF_AXIOM(node); - - const PXR_NS::PcpLayerStackSite& site = node.GetSite(); - const PXR_NS::PcpLayerStackRefPtr& layerStack = site.layerStack; - - const std::set& mutedLayers = layerStack->GetMutedLayers(); - if (mutedLayers.size() > 0) { - return true; - } - } - return false; -} -#endif -} // anonymous namespace - namespace MAYAUSD_NS_DEF { namespace ufe { @@ -78,6 +54,8 @@ void UsdUndoDeleteCommand::execute() if (!_prim.IsValid()) return; + enforceMutedLayer(_prim, "remove"); + MayaUsd::ufe::InAddOrDeleteOperation ad; UsdUndoBlock undoBlock(&_undoableItem); @@ -86,13 +64,6 @@ void UsdUndoDeleteCommand::execute() const auto& stage = _prim.GetStage(); auto targetPrimSpec = stage->GetEditTarget().GetPrimSpecForScenePath(_prim.GetPath()); - if (hasLayersMuted(_prim)) { - const std::string error = TfStringPrintf( - "Cannot remove prim \"%s\" because there are muted layers.", _prim.GetPath().GetText()); - TF_WARN("%s", error.c_str()); - throw std::runtime_error(error); - } - if (MayaUsd::ufe::applyCommandRestrictionNoThrow(_prim, "delete")) { #ifdef UFE_V4_FEATURES_AVAILABLE #if (UFE_PREVIEW_VERSION_NUM >= 4024) diff --git a/lib/mayaUsd/ufe/UsdUndoInsertChildCommand.cpp b/lib/mayaUsd/ufe/UsdUndoInsertChildCommand.cpp index 9b290d7626..6eb62ee303 100644 --- a/lib/mayaUsd/ufe/UsdUndoInsertChildCommand.cpp +++ b/lib/mayaUsd/ufe/UsdUndoInsertChildCommand.cpp @@ -196,6 +196,8 @@ static UsdSceneItem::Ptr doInsertion( const UsdPrim srcPrim = ufePathToPrim(srcUfePath); const UsdStagePtr stage = srcPrim.GetStage(); + enforceMutedLayer(srcPrim, "reparent"); + // Make sure the load state of the reparented prim will be preserved. // We copy all rules that applied to it specifically and remove the rules // that applied to it specifically. diff --git a/lib/mayaUsd/ufe/UsdUndoRenameCommand.cpp b/lib/mayaUsd/ufe/UsdUndoRenameCommand.cpp index 034c36ec9d..0a05fcb501 100644 --- a/lib/mayaUsd/ufe/UsdUndoRenameCommand.cpp +++ b/lib/mayaUsd/ufe/UsdUndoRenameCommand.cpp @@ -146,6 +146,8 @@ static void doUsdRename( const Ufe::Path srcPath, const Ufe::Path dstPath) { + enforceMutedLayer(prim, "rename"); + // 1- open a changeblock to delay sending notifications. // 2- update the Internal References paths (if any) first // 3- set the new name diff --git a/lib/mayaUsd/utils/layers.cpp b/lib/mayaUsd/utils/layers.cpp index 57a8be3972..586e516582 100644 --- a/lib/mayaUsd/utils/layers.cpp +++ b/lib/mayaUsd/utils/layers.cpp @@ -85,6 +85,37 @@ getAllSublayers(const std::vector& layerPaths, bool includeParents) return layers; } +bool hasMutedLayer(const PXR_NS::UsdPrim& prim) +{ + const PXR_NS::PcpPrimIndex& primIndex = prim.GetPrimIndex(); + + for (const PXR_NS::PcpNodeRef node : primIndex.GetNodeRange()) { + if (!node) + continue; + + const PXR_NS::PcpLayerStackRefPtr& layerStack = node.GetSite().layerStack; + if (!layerStack) + continue; + + const std::set& mutedLayers = layerStack->GetMutedLayers(); + if (mutedLayers.size() > 0) + return true; + } + return false; +} + +void enforceMutedLayer(const PXR_NS::UsdPrim& prim, const char* command) +{ + if (hasMutedLayer(prim)) { + const std::string error = TfStringPrintf( + "Cannot %s prim \"%s\" because there is at least one muted layer.", + command && command[0] ? command : "modify", + prim.GetPath().GetText()); + TF_WARN("%s", error.c_str()); + throw std::runtime_error(error); + } +} + void applyToAllPrimSpecs(const UsdPrim& prim, const PrimSpecFunc& func) { const SdfPrimSpecHandleVector primStack = prim.GetPrimStack(); diff --git a/lib/mayaUsd/utils/layers.h b/lib/mayaUsd/utils/layers.h index 471578c762..b0a73384c2 100644 --- a/lib/mayaUsd/utils/layers.h +++ b/lib/mayaUsd/utils/layers.h @@ -63,6 +63,26 @@ MAYAUSD_CORE_PUBLIC std::set getAllSublayerRefs(const PXR_NS::SdfLayerRefPtr& layer, bool includeTopLayer = false); +/** + * Verify if the given prim has opinions on a muted layer. + * + * @param prim The prim to be verified. + * + * @return true if there is at least one muted layer. + */ + +bool hasMutedLayer(const PXR_NS::UsdPrim& prim); + +/** + * Enforce that command cannot operate if the given prim has opinions on a muted layer by throwing + * an exception. + * + * @param prim The prim to be verified. + * @param command The name of the command. Will use "modify" if null or empty. + */ + +void enforceMutedLayer(const PXR_NS::UsdPrim& prim, const char* command); + /** * Apply the given function to all the opinions about the given prim. * diff --git a/test/lib/ufe/testDeleteCmd.py b/test/lib/ufe/testDeleteCmd.py index b872f42e05..841bf536f8 100644 --- a/test/lib/ufe/testDeleteCmd.py +++ b/test/lib/ufe/testDeleteCmd.py @@ -226,12 +226,7 @@ def testDeleteRestrictionDifferentLayer(self): ufeObs = TestObserver() ufe.Scene.addObserver(ufeObs) - # validate the default edit target to be the Rootlayer. - mayaPathSegment = mayaUtils.createUfePathSegment('|Tree_usd|Tree_usdShape') - stage = mayaUsd.ufe.getStage(str(mayaPathSegment)) - self.assertTrue(stage) - - # add child defined on a new layer + # retrieve the stage mayaPathSegment = mayaUtils.createUfePathSegment('|Tree_usd|Tree_usdShape') stage = mayaUsd.ufe.getStage(str(mayaPathSegment)) self.assertTrue(stage) @@ -531,5 +526,51 @@ def testDeleteAndRemoveConnections(self): self.assertFalse(surface2Prim.HasProperty('outputs:out')) self.assertFalse(surface3Prim.HasProperty('inputs:bsdf')) + def testDeleteRestrictionMutedLayer(self): + ''' + Test delete restriction - we don't allow removal of a prim + when there are opinions on a muted layer. + ''' + + # Create a stage with a xform prim named A + cmds.file(new=True, force=True) + import mayaUsd_createStageWithNewLayer + + proxyShapePathStr = mayaUsd_createStageWithNewLayer.createStageWithNewLayer() + stage = mayaUsd.lib.GetPrim(proxyShapePathStr).GetStage() + self.assertTrue(stage) + stage.DefinePrim('/A', 'Xform') + + # Add two new layers + usdFormat = Sdf.FileFormat.FindByExtension('usd') + topLayer = Sdf.Layer.New(usdFormat, 'Layer_1') + stage.GetRootLayer().subLayerPaths.append(topLayer.identifier) + + usdFormat = Sdf.FileFormat.FindByExtension('usd') + bottomLayer = Sdf.Layer.New(usdFormat, 'Layer_2') + stage.GetRootLayer().subLayerPaths.append(bottomLayer.identifier) + + # Create a sphere on the bottom layer + stage.SetEditTarget(bottomLayer) + self.assertEqual(stage.GetEditTarget().GetLayer(), bottomLayer) + spherePrim = stage.DefinePrim('/A/ball', 'Sphere') + self.assertIsNotNone(spherePrim) + + # Author an opinion on the top layer + stage.SetEditTarget(topLayer) + self.assertEqual(stage.GetEditTarget().GetLayer(), topLayer) + spherePrim.GetAttribute('radius').Set(4.) + + # Set target to bottom layer and mute the top layer + stage.SetEditTarget(bottomLayer) + self.assertEqual(stage.GetEditTarget().GetLayer(), bottomLayer) + # Note: mute by passing through the stage, otherwise the stage won't get recomposed + stage.MuteLayer(topLayer.identifier) + + # Try to delete the prim with muted opinion: it should fail + with self.assertRaises(RuntimeError): + cmds.delete('%s,/A/ball' % proxyShapePathStr) + self.assertTrue(stage.GetPrimAtPath('/A/ball')) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/test/lib/ufe/testGroupCmd.py b/test/lib/ufe/testGroupCmd.py index e9bf39b3e7..2a80cdeade 100644 --- a/test/lib/ufe/testGroupCmd.py +++ b/test/lib/ufe/testGroupCmd.py @@ -1033,5 +1033,64 @@ def testGroupPivotOrigin(self): # With group pivot origin the group pivot is at the origin. self.runTestGroupPivotOptions("doGroup 0 1 1", [0, 0, 0]) + def testGroupRestrictionMutedLayer(self): + ''' + Test group restriction - we don't allow grouping of a prim + when there are opinions on a muted layer. + ''' + + # Create a stage + cmds.file(new=True, force=True) + import mayaUsd_createStageWithNewLayer + + proxyShapePathStr = mayaUsd_createStageWithNewLayer.createStageWithNewLayer() + stage = mayaUsd.lib.GetPrim(proxyShapePathStr).GetStage() + self.assertTrue(stage) + + # Helpers + def createLayer(index): + layer = Sdf.Layer.CreateAnonymous() + stage.GetRootLayer().subLayerPaths.append(layer.identifier) + return layer + + def targetSubLayer(layer): + stage.SetEditTarget(layer) + self.assertEqual(stage.GetEditTarget().GetLayer(), layer) + layer = None + + def muteSubLayer(layer): + # Note: mute by passing through the stage, otherwise the stage won't get recomposed + stage.MuteLayer(layer.identifier) + + def setSphereRadius(radius): + spherePrim = stage.GetPrimAtPath('/A/ball') + spherePrim.GetAttribute('radius').Set(radius) + spherePrim = None + + # Add two new layers + topLayer = createLayer(0) + bottomLayer = createLayer(1) + + # Create a xform prim named A and a sphere on the bottom layer + targetSubLayer(bottomLayer) + stage.DefinePrim('/A', 'Xform') + stage.DefinePrim('/A/ball', 'Sphere') + setSphereRadius(7.12) + + targetSubLayer(topLayer) + setSphereRadius(4.32) + + # Set target to bottom layer and mute the top layer + targetSubLayer(bottomLayer) + muteSubLayer(topLayer) + + # Try to group the prim with muted opinion: it should fail + with self.assertRaises(RuntimeError): + cmds.group('%s,/A/ball' % proxyShapePathStr) + + self.assertTrue(stage.GetPrimAtPath('/A/ball')) + self.assertFalse(stage.GetPrimAtPath('/A/group1/ball')) + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/test/lib/ufe/testRename.py b/test/lib/ufe/testRename.py index d03b987616..eee590b7cb 100644 --- a/test/lib/ufe/testRename.py +++ b/test/lib/ufe/testRename.py @@ -974,6 +974,64 @@ def testUfeRenameCommandAPI(self): self.assertIsNotNone(carotteItem) self.assertEqual(carotteItem, renamedItem) + def testRenameRestrictionMutedLayer(self): + ''' + Test rename restriction - we don't allow renaming a prim + when there are opinions on a muted layer. + ''' + + # Create a stage + cmds.file(new=True, force=True) + import mayaUsd_createStageWithNewLayer + + proxyShapePathStr = mayaUsd_createStageWithNewLayer.createStageWithNewLayer() + stage = mayaUsd.lib.GetPrim(proxyShapePathStr).GetStage() + self.assertTrue(stage) + + # Helpers + def createLayer(index): + layer = Sdf.Layer.CreateAnonymous() + stage.GetRootLayer().subLayerPaths.append(layer.identifier) + return layer + + def targetSubLayer(layer): + stage.SetEditTarget(layer) + self.assertEqual(stage.GetEditTarget().GetLayer(), layer) + layer = None + + def muteSubLayer(layer): + # Note: mute by passing through the stage, otherwise the stage won't get recomposed + stage.MuteLayer(layer.identifier) + + def setSphereRadius(radius): + spherePrim = stage.GetPrimAtPath('/A/ball') + spherePrim.GetAttribute('radius').Set(radius) + spherePrim = None + + # Add two new layers + topLayer = createLayer(0) + bottomLayer = createLayer(1) + + # Create a xform prim named A and a sphere on the bottom layer + targetSubLayer(bottomLayer) + stage.DefinePrim('/A', 'Xform') + stage.DefinePrim('/A/ball', 'Sphere') + setSphereRadius(7.12) + + targetSubLayer(topLayer) + setSphereRadius(4.32) + + # Set target to bottom layer and mute the top layer + targetSubLayer(bottomLayer) + muteSubLayer(topLayer) + + # Try to rename the prim with muted opinion: it should fail + with self.assertRaises(RuntimeError): + cmds.rename('%s,/A/ball' % proxyShapePathStr, 'ball2') + + self.assertTrue(stage.GetPrimAtPath('/A/ball')) + self.assertFalse(stage.GetPrimAtPath('/A/group1/ball')) + if __name__ == '__main__': unittest.main(verbosity=2) From bcc463dda8b2c158aae1a504611b331f40ac2098 Mon Sep 17 00:00:00 2001 From: Pierre Baillargeon Date: Mon, 13 Mar 2023 08:54:32 -0400 Subject: [PATCH 2/3] MAYA-128211 fix invalid non-ASCII character --- plugin/adsk/scripts/mayaUSDRegisterStrings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/adsk/scripts/mayaUSDRegisterStrings.py b/plugin/adsk/scripts/mayaUSDRegisterStrings.py index 546fcb57c8..7f716acb13 100644 --- a/plugin/adsk/scripts/mayaUSDRegisterStrings.py +++ b/plugin/adsk/scripts/mayaUSDRegisterStrings.py @@ -38,9 +38,9 @@ def mayaUSDRegisterStrings(): register("kMakePathRelativeToSceneFile", "Make Path Relative to Scene File") register("kMakePathRelativeToSceneFileAnn", "If enabled, path will be relative to your Maya scene file.\nIf this option is disabled, there is no Maya scene file and the path will be absolute.\nSave your Maya scene file to disk to make this option available.") register("kMakePathRelativeToEditTargetLayer", "Make Path Relative to Edit Target Layer Directory") - register("kMakePathRelativeToEditTargetLayerAnn", "Enable to activate relative pathing to your current edit target layer’s directory.\nIf this option is disabled, verify that your target layer is not anonymous and save it to disk.") + register("kMakePathRelativeToEditTargetLayerAnn", "Enable to activate relative pathing to your current edit target layer's directory.\nIf this option is disabled, verify that your target layer is not anonymous and save it to disk.") register("kMakePathRelativeToParentLayer", "Make Path Relative to Parent Layer Directory") - register("kMakePathRelativeToParentLayerAnn", "Enable to activate relative pathing to your current parent layer’s directory.\nIf this option is disabled, verify that your parent layer is not anonymous and save it to disk.") + register("kMakePathRelativeToParentLayerAnn", "Enable to activate relative pathing to your current parent layer's directory.\nIf this option is disabled, verify that your parent layer is not anonymous and save it to disk.") register("kUnresolvedPath", "Unresolved Path:") register("kUnresolvedPathAnn", "This field indicates the path with the file name currently chosen in your text input. Note: This is the string that will be written out to the file in the chosen directory in order to enable portability.") register("kResolvedPath", "Resolved Path:") From ae5380703362edb4b5ef767f46db90c840ab6e46 Mon Sep 17 00:00:00 2001 From: Pierre Baillargeon Date: Mon, 13 Mar 2023 12:00:21 -0400 Subject: [PATCH 3/3] MAYA-128211 unit test fix --- test/lib/ufe/testGroupCmd.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/lib/ufe/testGroupCmd.py b/test/lib/ufe/testGroupCmd.py index 2a80cdeade..23d02cf9ed 100644 --- a/test/lib/ufe/testGroupCmd.py +++ b/test/lib/ufe/testGroupCmd.py @@ -1033,6 +1033,7 @@ def testGroupPivotOrigin(self): # With group pivot origin the group pivot is at the origin. self.runTestGroupPivotOptions("doGroup 0 1 1", [0, 0, 0]) + @unittest.skipUnless(mayaUtils.mayaMajorVersion() >= 2023, 'Requires Maya fixes only available in Maya 2023 or greater.') def testGroupRestrictionMutedLayer(self): ''' Test group restriction - we don't allow grouping of a prim