Skip to content

Commit e31ce5b

Browse files
authored
Merge pull request #4651 from Autodesk/barbalt/dev/EMSUSD-3311-remove-opinion
EMSUSD-3311 - Add remove opinion + undo
2 parents b5405e7 + 2e8f5ea commit e31ce5b

2 files changed

Lines changed: 149 additions & 0 deletions

File tree

lib/usd/ui/debugTools/CompositionEditorCmd.cpp

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
#include "CompositionEditorCmd.h"
1717

1818
#include <mayaUsd/ufe/Utils.h>
19+
#include <mayaUsd/undo/MayaUsdUndoBlock.h>
1920

21+
#include <usdUfe/undo/UsdUndoManager.h>
22+
23+
#include <pxr/usd/sdf/layer.h>
2024
#include <pxr/usd/usd/prim.h>
2125

2226
// This is added to prevent multiple definitions of the MApiVersion string.
@@ -42,6 +46,9 @@
4246
#include <UsdDebugUI/ApplicationHost.h>
4347
#include <UsdDebugUI/CompositionEditorWidget.h>
4448

49+
#include <algorithm>
50+
#include <string>
51+
4552
namespace MAYAUSD_NS_DEF {
4653

4754
const MString CompositionEditorCmd::name("mayaUsdCompositionEditor");
@@ -55,6 +62,31 @@ constexpr auto kReloadFlagLong = "-reload";
5562

5663
const MString WORKSPACE_CONTROL_NAME = "mayaUsdCompositionEditor";
5764

65+
// RAII guard that opens a named Maya undo chunk
66+
class UndoChunkContext
67+
{
68+
private:
69+
// Maya's undo chunk names cannot contain spaces (the name is split at the
70+
// first space), so replace them with underscores before quoting.
71+
MString cleanChunkName(const std::string& label)
72+
{
73+
std::string name = label.empty() ? "USD Composition Edit" : label;
74+
std::replace(name.begin(), name.end(), ' ', '_');
75+
return MString("\"") + name.c_str() + "\"";
76+
}
77+
78+
public:
79+
explicit UndoChunkContext(const std::string& label)
80+
{
81+
MGlobal::executeCommand(
82+
MString("undoInfo -openChunk -chunkName ") + cleanChunkName(label), false, false);
83+
}
84+
~UndoChunkContext() { MGlobal::executeCommand("undoInfo -closeChunk", false, false); }
85+
86+
UndoChunkContext(const UndoChunkContext&) = delete;
87+
UndoChunkContext& operator=(const UndoChunkContext&) = delete;
88+
};
89+
5890
QPointer<Adsk::UsdDebug::CompositionEditorWidget> g_compositionEditorWidget;
5991
Ufe::Observer::Ptr g_selectionObserver;
6092

@@ -120,6 +152,26 @@ class MayaCompositionEditorHost : public Adsk::UsdDebug::ApplicationHost
120152
return QColor();
121153
}
122154

155+
bool executeInCmd(
156+
const std::string& editLabel,
157+
const std::string& layerId,
158+
const std::function<bool()>& edit) override
159+
{
160+
if (!edit) {
161+
return false;
162+
}
163+
164+
// Ensure the layer being edited has a UsdUndoStateDelegate so the inverse
165+
// of the edit is recorded
166+
if (PXR_NS::SdfLayerHandle layer = PXR_NS::SdfLayer::Find(layerId)) {
167+
UsdUfe::UsdUndoManager::instance().trackLayerStates(layer);
168+
}
169+
170+
UndoChunkContext undoChunk(editLabel);
171+
MayaUsdUndoBlock undoBlock;
172+
return edit();
173+
}
174+
123175
protected:
124176
MayaCompositionEditorHost() { injectInstance(this); }
125177
};

test/lib/testAdskUsdDebugTools.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717

1818
import unittest
1919

20+
import maya.cmds as cmds
21+
22+
from pxr import Sdf, Usd
23+
24+
import mayaUsd.lib as mayaUsdLib
25+
2026

2127
class testAdskUsdDebugTools(unittest.TestCase):
2228
"""
@@ -25,10 +31,101 @@ class testAdskUsdDebugTools(unittest.TestCase):
2531

2632
def testDebugToolsLoaded(self):
2733
try:
34+
# Import the USD Python bindings first so the boost.python to/from-Python
35+
# converters for USD types (e.g. SdfPath) are registered.
36+
from pxr import Usd, Sdf
2837
import AdskUsdDebug
2938
except Exception as e:
3039
self.fail(f"Autodesk USD Debug Tools module not available or failed to load. {e}")
3140

3241

42+
class testAdskUsdDebugToolsRemoveOpinionUndo(unittest.TestCase):
43+
"""
44+
Verify that removing an opinion through the Debug Tools composition editor
45+
can be undone.
46+
"""
47+
48+
@classmethod
49+
def setUpClass(cls):
50+
cmds.loadPlugin('mayaUsdPlugin')
51+
52+
def setUp(self):
53+
# Skip if the Debug Tools component was not shipped in this build; the
54+
# smoke test above already reports a hard failure when it is expected.
55+
try:
56+
import AdskUsdDebug
57+
except Exception as e:
58+
self.skipTest(f"Autodesk USD Debug Tools module not available: {e}")
59+
60+
cmds.file(force=True, new=True)
61+
62+
self.stage = Usd.Stage.CreateInMemory()
63+
64+
# Mirror executeInCmd step 1: track the layer being edited so its inverse
65+
# edits are recorded by the undo manager.
66+
mayaUsdLib.UsdUndoManager.trackLayerStates(self.stage.GetRootLayer())
67+
68+
cmds.select(clear=True)
69+
70+
def testRemoveOpinionUndoRedo(self):
71+
'''Removing a single property opinion is undoable and redoable.'''
72+
from AdskUsdDebug import RemoveOpinion
73+
74+
prim = self.stage.DefinePrim('/Foo', 'Xform')
75+
prim.CreateAttribute('radius', Sdf.ValueTypeNames.Double).Set(2.0)
76+
layerId = self.stage.GetRootLayer().identifier
77+
78+
self.assertTrue(prim.GetAttribute('radius').HasAuthoredValue())
79+
80+
nbCmds = cmds.undoInfo(q=True)
81+
82+
with mayaUsdLib.UsdUndoBlock():
83+
self.assertTrue(RemoveOpinion(self.stage, layerId, '/Foo.radius'))
84+
85+
# The opinion is gone and exactly one command was added to the queue.
86+
self.assertFalse(prim.GetAttribute('radius').HasAuthoredValue())
87+
self.assertEqual(cmds.undoInfo(q=True), nbCmds + 1)
88+
89+
# Undo restores the removed opinion (value included).
90+
cmds.undo()
91+
self.assertTrue(prim.GetAttribute('radius').HasAuthoredValue())
92+
self.assertEqual(prim.GetAttribute('radius').Get(), 2.0)
93+
94+
# Redo removes it again.
95+
cmds.redo()
96+
self.assertFalse(prim.GetAttribute('radius').HasAuthoredValue())
97+
98+
def testRemoveOpinionsUndoRedo(self):
99+
'''Removing several opinions in one edit is undoable as a single step.'''
100+
from AdskUsdDebug import OpinionRef, RemoveOpinions
101+
102+
prim = self.stage.DefinePrim('/Foo', 'Xform')
103+
prim.CreateAttribute('radius', Sdf.ValueTypeNames.Double).Set(2.0)
104+
prim.CreateAttribute('count', Sdf.ValueTypeNames.Int).Set(7)
105+
layerId = self.stage.GetRootLayer().identifier
106+
107+
nbCmds = cmds.undoInfo(q=True)
108+
109+
with mayaUsdLib.UsdUndoBlock():
110+
self.assertTrue(RemoveOpinions(
111+
self.stage,
112+
[
113+
OpinionRef(layerId, '/Foo.radius'),
114+
OpinionRef(layerId, '/Foo.count'),
115+
]))
116+
117+
# Both opinions removed, batched into a single undoable command.
118+
self.assertFalse(prim.GetAttribute('radius').HasAuthoredValue())
119+
self.assertFalse(prim.GetAttribute('count').HasAuthoredValue())
120+
self.assertEqual(cmds.undoInfo(q=True), nbCmds + 1)
121+
122+
# A single undo restores both opinions.
123+
cmds.undo()
124+
self.assertTrue(prim.GetAttribute('radius').HasAuthoredValue())
125+
self.assertTrue(prim.GetAttribute('count').HasAuthoredValue())
126+
self.assertEqual(prim.GetAttribute('radius').Get(), 2.0)
127+
self.assertEqual(prim.GetAttribute('count').Get(), 7)
128+
129+
33130
if __name__ == '__main__':
34131
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)