From 32f93a06e567e7cc27c700e498a5733984161570 Mon Sep 17 00:00:00 2001 From: Kevin Derler Date: Wed, 17 Jun 2026 19:11:05 -0400 Subject: [PATCH 1/7] Undo/redo support for the RenderSetup. --- lib/usd/ui/renderSetup/CMakeLists.txt | 3 + lib/usd/ui/renderSetup/MayaEditCommitter.cpp | 102 +++++++++++++ lib/usd/ui/renderSetup/MayaEditCommitter.h | 55 +++++++ .../ui/renderSetup/renderSetupWindowCmd.cpp | 17 ++- lib/usdUfe/ufe/CMakeLists.txt | 2 + .../ufe/UsdLabeledEditUndoableCommand.cpp | 36 +++++ .../ufe/UsdLabeledEditUndoableCommand.h | 49 +++++++ lib/usdUfe/undo/CMakeLists.txt | 2 + lib/usdUfe/undo/UsdUndoUtils.cpp | 35 +++++ lib/usdUfe/undo/UsdUndoUtils.h | 34 +++++ test/lib/CMakeLists.txt | 1 + test/lib/mayaUsd/CMakeLists.txt | 3 + test/lib/mayaUsd/ui/CMakeLists.txt | 18 +++ test/lib/mayaUsd/ui/testRenderSetupUndo.py | 137 ++++++++++++++++++ test/lib/usdUfe/CMakeLists.txt | 1 + test/lib/usdUfe/undo/CMakeLists.txt | 36 +++++ test/lib/usdUfe/undo/main.cpp | 16 ++ test/lib/usdUfe/undo/test_UsdUndoUtils.cpp | 112 ++++++++++++++ 18 files changed, 655 insertions(+), 4 deletions(-) create mode 100644 lib/usd/ui/renderSetup/MayaEditCommitter.cpp create mode 100644 lib/usd/ui/renderSetup/MayaEditCommitter.h create mode 100644 lib/usdUfe/ufe/UsdLabeledEditUndoableCommand.cpp create mode 100644 lib/usdUfe/ufe/UsdLabeledEditUndoableCommand.h create mode 100644 lib/usdUfe/undo/UsdUndoUtils.cpp create mode 100644 lib/usdUfe/undo/UsdUndoUtils.h create mode 100644 test/lib/mayaUsd/ui/CMakeLists.txt create mode 100644 test/lib/mayaUsd/ui/testRenderSetupUndo.py create mode 100644 test/lib/usdUfe/CMakeLists.txt create mode 100644 test/lib/usdUfe/undo/CMakeLists.txt create mode 100644 test/lib/usdUfe/undo/main.cpp create mode 100644 test/lib/usdUfe/undo/test_UsdUndoUtils.cpp diff --git a/lib/usd/ui/renderSetup/CMakeLists.txt b/lib/usd/ui/renderSetup/CMakeLists.txt index b0537e9e2a..759f5f59d0 100644 --- a/lib/usd/ui/renderSetup/CMakeLists.txt +++ b/lib/usd/ui/renderSetup/CMakeLists.txt @@ -17,11 +17,14 @@ target_sources(${PROJECT_NAME} PRIVATE + MayaEditCommitter.cpp + MayaEditCommitter.h renderSetupWindowCmd.cpp ) mayaUsd_promoteHeaderList( HEADERS + MayaEditCommitter.h renderSetupWindowCmd.h BASEDIR ${PROJECT_NAME}/ui diff --git a/lib/usd/ui/renderSetup/MayaEditCommitter.cpp b/lib/usd/ui/renderSetup/MayaEditCommitter.cpp new file mode 100644 index 0000000000..f92d444a21 --- /dev/null +++ b/lib/usd/ui/renderSetup/MayaEditCommitter.cpp @@ -0,0 +1,102 @@ +// +// Copyright 2026 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include "MayaEditCommitter.h" + +#include + +#include + +#include +#include + +#include +#include + +namespace { + +// Maya does not support spaces in undo chunk names. Backslash and double-quote +// are escaped so the label is safe to embed in a MEL double-quoted string. +MString cleanChunkName(const std::string& label) +{ + QString name = QString::fromStdString(label); + name.replace(QLatin1Char('\\'), QLatin1String("\\\\")); + name.replace(QLatin1Char('"'), QLatin1String("\\\"")); + name.replace(QLatin1Char(' '), QLatin1Char('_')); + return MString(("\"" + name.toStdString() + "\"").c_str()); +} + +void openUndoChunk(const std::string& label) +{ + if (label.empty()) { + MGlobal::executeCommand("undoInfo -openChunk", false, false); + } else { + MGlobal::executeCommand( + MString("undoInfo -openChunk -chunkName ") + cleanChunkName(label), false, false); + } +} + +struct UndoChunkGuard +{ + ~UndoChunkGuard() { MGlobal::executeCommand("undoInfo -closeChunk", false, false); } +}; + +} // namespace + +namespace MayaUsdRenderSetup { + +MayaEditCommitter::MayaEditCommitter(QObject* parent) + : QObject(parent) +{ +} + +void MayaEditCommitter::setStages(const std::vector& stages) +{ + m_stages.clear(); + m_stages.reserve(stages.size()); + for (const auto& hostStage : stages) { + if (hostStage.stage) { + m_stages.push_back(hostStage.stage); + } + } +} + +void MayaEditCommitter::commit(const std::string& undoLabel, std::function doEdit) +{ + if (!doEdit || m_stages.empty()) { + return; + } + + ++m_inFlightCount; + // Schedule the decrement before doEdit() so it fires even if doEdit() throws. + // The one-event-loop-cycle delay keeps isLocalEditInFlight() true through the + // USD-notice burst that follows the edit, so the notice bridge treats the + // resulting refresh as self-originated and skips re-entrancy. + QTimer::singleShot(0, this, [this]() { + if (m_inFlightCount > 0) { + --m_inFlightCount; + } + }); + + UsdUfe::trackStagesEditTargets(m_stages); + + openUndoChunk(undoLabel); + const UndoChunkGuard undoChunkGuard; + MayaUsd::MayaUsdUndoBlock block; + doEdit(); +} + +} // namespace MayaUsdRenderSetup diff --git a/lib/usd/ui/renderSetup/MayaEditCommitter.h b/lib/usd/ui/renderSetup/MayaEditCommitter.h new file mode 100644 index 0000000000..3febb0a792 --- /dev/null +++ b/lib/usd/ui/renderSetup/MayaEditCommitter.h @@ -0,0 +1,55 @@ +// +// Copyright 2026 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#ifndef MAYAUSD_UI_RENDERSETUP_MAYAEDITCOMMITTER_H +#define MAYAUSD_UI_RENDERSETUP_MAYAEDITCOMMITTER_H + +#include + +#include +#include +#include + +#include +#include +#include + +namespace MayaUsdRenderSetup { + +//! MayaUSD implementation of Adsk::IEditCommitter for the Render Setup UI. +//! Captures USD edits through UsdUfe and flushes them onto the Maya undo stack +//! via MayaUsdUndoBlock. +class MayaEditCommitter + : public QObject + , public Adsk::IEditCommitter +{ + Q_OBJECT +public: + explicit MayaEditCommitter(QObject* parent = nullptr); + + void setStages(const std::vector& stages); + + void commit(const std::string& undoLabel, std::function doEdit) override; + bool isLocalEditInFlight() const override { return m_inFlightCount > 0; } + +private: + std::vector m_stages; + int m_inFlightCount { 0 }; +}; + +} // namespace MayaUsdRenderSetup + +#endif // MAYAUSD_UI_RENDERSETUP_MAYAEDITCOMMITTER_H diff --git a/lib/usd/ui/renderSetup/renderSetupWindowCmd.cpp b/lib/usd/ui/renderSetup/renderSetupWindowCmd.cpp index 0023c5ef91..ec34449ea6 100644 --- a/lib/usd/ui/renderSetup/renderSetupWindowCmd.cpp +++ b/lib/usd/ui/renderSetup/renderSetupWindowCmd.cpp @@ -15,6 +15,8 @@ // #include "renderSetupWindowCmd.h" +#include "MayaEditCommitter.h" + #include #include #include @@ -88,12 +90,17 @@ class RenderSetupWindow static void onSceneChangedCB(void* clientData); private: - void applyStages() { _tree->setStages(_hostStages); } + void applyStages() + { + _editCommitter->setStages(_hostStages); + _tree->setStages(_hostStages); + } private: - Adsk::RenderSetupWidget* _tree; - std::vector _hostStages; - std::vector _sceneCallbackIds; + Adsk::RenderSetupWidget* _tree; + MayaUsdRenderSetup::MayaEditCommitter* _editCommitter { nullptr }; + std::vector _hostStages; + std::vector _sceneCallbackIds; }; RenderSetupWindow::RenderSetupWindow(QWidget* parent) @@ -101,6 +108,8 @@ RenderSetupWindow::RenderSetupWindow(QWidget* parent) { // Create the render setup widget and set it as the central widget of the window. _tree = new Adsk::RenderSetupWidget(this); + _editCommitter = new MayaUsdRenderSetup::MayaEditCommitter(nullptr); + _tree->setEditCommitter(std::unique_ptr(_editCommitter)); setCentralWidget(_tree); _tree->show(); diff --git a/lib/usdUfe/ufe/CMakeLists.txt b/lib/usdUfe/ufe/CMakeLists.txt index 8866dfa1b4..87a76d6b56 100644 --- a/lib/usdUfe/ufe/CMakeLists.txt +++ b/lib/usdUfe/ufe/CMakeLists.txt @@ -49,6 +49,7 @@ target_sources(${PROJECT_NAME} UsdUndoVisibleCommand.cpp UsdUndoSetDefaultPrimCommand.cpp UsdUndoClearDefaultPrimCommand.cpp + UsdLabeledEditUndoableCommand.cpp UsdUndoableCommand.cpp Utils.cpp ) @@ -219,6 +220,7 @@ set(HEADERS UsdUndoVisibleCommand.h UsdUndoSetDefaultPrimCommand.h UsdUndoClearDefaultPrimCommand.h + UsdLabeledEditUndoableCommand.h UsdUndoableCommand.h Utils.h ) diff --git a/lib/usdUfe/ufe/UsdLabeledEditUndoableCommand.cpp b/lib/usdUfe/ufe/UsdLabeledEditUndoableCommand.cpp new file mode 100644 index 0000000000..c2173b284c --- /dev/null +++ b/lib/usdUfe/ufe/UsdLabeledEditUndoableCommand.cpp @@ -0,0 +1,36 @@ +// +// Copyright 2026 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include "UsdLabeledEditUndoableCommand.h" + +namespace USDUFE_NS_DEF { + +UsdLabeledEditUndoableCommand::UsdLabeledEditUndoableCommand( + std::string label, + std::function edit) + : _label(std::move(label)) + , _edit(std::move(edit)) +{ +} + +void UsdLabeledEditUndoableCommand::executeImplementation() +{ + if (_edit) { + _edit(); + } +} + +} // namespace USDUFE_NS_DEF diff --git a/lib/usdUfe/ufe/UsdLabeledEditUndoableCommand.h b/lib/usdUfe/ufe/UsdLabeledEditUndoableCommand.h new file mode 100644 index 0000000000..08b399ca34 --- /dev/null +++ b/lib/usdUfe/ufe/UsdLabeledEditUndoableCommand.h @@ -0,0 +1,49 @@ +// +// Copyright 2026 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#ifndef USDUFE_USDLABELEDEDITUNDOABLECOMMAND_H +#define USDUFE_USDLABELEDEDITUNDOABLECOMMAND_H + +#include +#include +#include + +#include + +#include +#include + +namespace USDUFE_NS_DEF { + +//! UFE undo command that runs a host-supplied edit lambda and exposes +//! a human-readable undo label through commandString(). +class USDUFE_PUBLIC UsdLabeledEditUndoableCommand : public UsdUndoableCommand +{ +public: + UsdLabeledEditUndoableCommand(std::string label, std::function edit); + + void executeImplementation() override; + + UFE_V4(std::string commandString() const override { return _label; }) + +private: + std::string _label; + std::function _edit; +}; + +} // namespace USDUFE_NS_DEF + +#endif // USDUFE_USDLABELEDEDITUNDOABLECOMMAND_H diff --git a/lib/usdUfe/undo/CMakeLists.txt b/lib/usdUfe/undo/CMakeLists.txt index 042bb2fac0..abb56e8b3e 100644 --- a/lib/usdUfe/undo/CMakeLists.txt +++ b/lib/usdUfe/undo/CMakeLists.txt @@ -7,6 +7,7 @@ target_sources(${PROJECT_NAME} UsdUndoManager.cpp UsdUndoStateDelegate.cpp UsdUndoableItem.cpp + UsdUndoUtils.cpp ) # ----------------------------------------------------------------------------- @@ -17,6 +18,7 @@ set(HEADERS UsdUndoManager.h UsdUndoStateDelegate.h UsdUndoableItem.h + UsdUndoUtils.h ) # ----------------------------------------------------------------------------- diff --git a/lib/usdUfe/undo/UsdUndoUtils.cpp b/lib/usdUfe/undo/UsdUndoUtils.cpp new file mode 100644 index 0000000000..af93a942c9 --- /dev/null +++ b/lib/usdUfe/undo/UsdUndoUtils.cpp @@ -0,0 +1,35 @@ +// +// Copyright 2026 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include "UsdUndoUtils.h" + +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace USDUFE_NS_DEF { + +void trackStagesEditTargets(const std::vector& stages) +{ + for (const auto& stage : stages) { + if (!stage) { + continue; + } + UsdUndoManager::instance().trackLayerStates(stage->GetEditTarget().GetLayer()); + } +} + +} // namespace USDUFE_NS_DEF diff --git a/lib/usdUfe/undo/UsdUndoUtils.h b/lib/usdUfe/undo/UsdUndoUtils.h new file mode 100644 index 0000000000..ef05ce7ea7 --- /dev/null +++ b/lib/usdUfe/undo/UsdUndoUtils.h @@ -0,0 +1,34 @@ +// +// Copyright 2026 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#ifndef USDUFE_UNDO_USDUNDOUTILS_H +#define USDUFE_UNDO_USDUNDOUTILS_H + +#include + +#include + +#include + +namespace USDUFE_NS_DEF { + +//! Installs a UsdUndoStateDelegate on each stage's current edit-target +//! layer. Repeated calls for the same layer are idempotent. +USDUFE_PUBLIC void trackStagesEditTargets(const std::vector& stages); + +} // namespace USDUFE_NS_DEF + +#endif // USDUFE_UNDO_USDUNDOUTILS_H diff --git a/test/lib/CMakeLists.txt b/test/lib/CMakeLists.txt index b92f0a3f75..e731ac8173 100644 --- a/test/lib/CMakeLists.txt +++ b/test/lib/CMakeLists.txt @@ -203,6 +203,7 @@ endforeach() add_subdirectory(ufe) add_subdirectory(mayaUsd) add_subdirectory(usd) +add_subdirectory(usdUfe) if (BUILD_MAYAUSDAPI_LIBRARY) add_subdirectory(mayaUsdAPI) endif() diff --git a/test/lib/mayaUsd/CMakeLists.txt b/test/lib/mayaUsd/CMakeLists.txt index 7b9fc51d3c..733e33601b 100644 --- a/test/lib/mayaUsd/CMakeLists.txt +++ b/test/lib/mayaUsd/CMakeLists.txt @@ -3,3 +3,6 @@ add_subdirectory(nodes) add_subdirectory(render) add_subdirectory(undo) add_subdirectory(utils) +if (AdskUsdRenderSetup_FOUND) + add_subdirectory(ui) +endif() diff --git a/test/lib/mayaUsd/ui/CMakeLists.txt b/test/lib/mayaUsd/ui/CMakeLists.txt new file mode 100644 index 0000000000..25d6a9bfd6 --- /dev/null +++ b/test/lib/mayaUsd/ui/CMakeLists.txt @@ -0,0 +1,18 @@ +if (AdskUsdRenderSetup_FOUND) + set(TEST_SCRIPT_FILES + testRenderSetupUndo.py + ) + + foreach(script ${TEST_SCRIPT_FILES}) + mayaUsd_get_unittest_target(target ${script}) + mayaUsd_add_test(${target} + PYTHON_MODULE ${target} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ENV + "LD_LIBRARY_PATH=${ADDITIONAL_LD_LIBRARY_PATH}" + ) + + set_property(TEST ${target} APPEND PROPERTY LABELS undo) + set_property(TEST ${target} APPEND PROPERTY LABELS SharedComponents) + endforeach() +endif() diff --git a/test/lib/mayaUsd/ui/testRenderSetupUndo.py b/test/lib/mayaUsd/ui/testRenderSetupUndo.py new file mode 100644 index 0000000000..5b4b73c1d8 --- /dev/null +++ b/test/lib/mayaUsd/ui/testRenderSetupUndo.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python + +# +# Copyright 2026 Autodesk +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import unittest + +import maya.cmds as cmds +import mayaUsd_createStageWithNewLayer + +from pxr import Gf, Sdf, UsdGeom, UsdRender + +import mayaUsd.lib as mayaUsdLib +from mayaUsd.lib import UsdDefaultRenderSettings + + +def _authorRenderSettings(stage): + '''Author a minimal render-settings graph on stage.''' + settings = UsdRender.Settings.Define(stage, Sdf.Path('/Render/Settings')) + UsdGeom.Camera.Define(stage, Sdf.Path('/Camera')) + return settings + + +def _mayaEditCommit(stage, doEdit, undoLabel='Edit'): + '''Protocol-level simulation of MayaEditCommitter::commit() for testing. + + Exercises the USD undo-capture flow (trackLayerStates + UsdUndoBlock wrapped + in a Maya undo chunk) without driving the C++ class directly, since + MayaEditCommitter has no Python bindings. Tests that use this helper cover + the undo/redo correctness of the capture protocol; they do not exercise + MayaEditCommitter-specific behaviour such as chunk-name sanitization or the + isLocalEditInFlight counter. + ''' + mayaUsdLib.UsdUndoManager.trackLayerStates(stage.GetEditTarget().GetLayer()) + chunkName = undoLabel.replace(' ', '_') + cmds.undoInfo(openChunk=True, chunkName=chunkName) + try: + with mayaUsdLib.UsdUndoBlock(): + doEdit() + finally: + cmds.undoInfo(closeChunk=True) + + +class TestRenderSetupUndo(unittest.TestCase): + '''Undo/redo for Render Setup-style USD edits on proxy-shape stages. + + Tests use _mayaEditCommit() to simulate the capture protocol. Full + integration coverage of MayaEditCommitter::commit() (chunk-name escaping, + isLocalEditInFlight gating) requires C++ test infrastructure. + ''' + + @classmethod + def setUpClass(cls): + cmds.loadPlugin('mayaUsdPlugin') + + def setUp(self): + cmds.file(force=True, new=True) + cmds.select(clear=True) + + def _createProxyStage(self): + shapeNode = mayaUsd_createStageWithNewLayer.createStageWithNewLayer() + stage = mayaUsdLib.GetPrim(shapeNode).GetStage() + settings = _authorRenderSettings(stage) + return stage, settings + + def testAttributeEditUndoRedo(self): + stage, settings = self._createProxyStage() + nbCmds = cmds.undoInfo(q=True) + + _mayaEditCommit(stage, lambda: settings.GetResolutionAttr().Set(Gf.Vec2i(1280, 720))) + + self.assertEqual(settings.GetResolutionAttr().Get(), Gf.Vec2i(1280, 720)) + self.assertEqual(cmds.undoInfo(q=True), nbCmds + 1) + + cmds.undo() + self.assertFalse(settings.GetResolutionAttr().HasAuthoredValue()) + + cmds.redo() + self.assertEqual(settings.GetResolutionAttr().Get(), Gf.Vec2i(1280, 720)) + + def testCameraRelationshipUndoRedo(self): + stage, settings = self._createProxyStage() + cameraPath = Sdf.Path('/Camera') + + _mayaEditCommit(stage, lambda: settings.GetCameraRel().SetTargets([cameraPath])) + self.assertEqual(settings.GetCameraRel().GetTargets(), [cameraPath]) + + cmds.undo() + self.assertEqual(settings.GetCameraRel().GetTargets(), []) + + cmds.redo() + self.assertEqual(settings.GetCameraRel().GetTargets(), [cameraPath]) + + @unittest.skipUnless( + hasattr(UsdDefaultRenderSettings, 'getUsdStage'), + 'Scene Render Settings stage requires MAYA_HAS_USD_SETTINGS_NODES') + def testSceneRenderSettingsUntrackedEditNotOnMayaUndoQueue(self): + '''A direct USD edit on the Scene Render Settings stage made without first + calling trackLayerStates is not captured and does not push a Maya undo entry. + + This tests the untracked path. When MayaEditCommitter is used and the + settings stage is included in setStages(), trackStagesEditTargets() installs + the delegate and the edit IS Maya-undoable. That path is blocked today + because UsdSceneSettingsManager does not yet feed the settings stage into + the Render Setup host-stage list (see Known limitation in README-USD-Undo.md). + ''' + stage = UsdDefaultRenderSettings.getUsdStage() + settings = UsdDefaultRenderSettings.getDefaultRenderSettingsPrim() + self.assertTrue(settings.IsValid()) + renderSettings = UsdRender.Settings(settings) + + nbCmds = cmds.undoInfo(q=True) + + # Edit without tracking — no delegate is installed, so nothing is captured. + with mayaUsdLib.UsdUndoBlock(): + renderSettings.GetResolutionAttr().Set(Gf.Vec2i(640, 480)) + + self.assertEqual(renderSettings.GetResolutionAttr().Get(), Gf.Vec2i(640, 480)) + # No undo entry should have been pushed. + self.assertEqual(cmds.undoInfo(q=True), nbCmds) + + cmds.undo() + # Value unchanged because nothing was on the undo queue. + self.assertEqual(renderSettings.GetResolutionAttr().Get(), Gf.Vec2i(640, 480)) diff --git a/test/lib/usdUfe/CMakeLists.txt b/test/lib/usdUfe/CMakeLists.txt new file mode 100644 index 0000000000..5b9290cdf1 --- /dev/null +++ b/test/lib/usdUfe/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(undo) diff --git a/test/lib/usdUfe/undo/CMakeLists.txt b/test/lib/usdUfe/undo/CMakeLists.txt new file mode 100644 index 0000000000..4f5f9138c7 --- /dev/null +++ b/test/lib/usdUfe/undo/CMakeLists.txt @@ -0,0 +1,36 @@ +function(add_mayaUsdUfeUndo_test TARGET_NAME) + add_executable(${TARGET_NAME}) + + target_sources(${TARGET_NAME} + PRIVATE + main.cpp + ${ARGN} + ) + + mayaUsd_compile_config(${TARGET_NAME}) + + target_compile_definitions(${TARGET_NAME} + PRIVATE + $<$:TBB_USE_DEBUG> + $<$:BOOST_DEBUG_PYTHON> + $<$:BOOST_LINKING_PYTHON> + ) + + target_link_libraries(${TARGET_NAME} + PRIVATE + GTest::GTest + usdUfe + usdRender + ) + + mayaUsd_add_test(${TARGET_NAME} + COMMAND $ + ENV + "LD_LIBRARY_PATH=${ADDITIONAL_LD_LIBRARY_PATH}" + ) +endfunction() + +add_mayaUsdUfeUndo_test( + testUsdUfeUndo + test_UsdUndoUtils.cpp +) diff --git a/test/lib/usdUfe/undo/main.cpp b/test/lib/usdUfe/undo/main.cpp new file mode 100644 index 0000000000..a5ce5953b2 --- /dev/null +++ b/test/lib/usdUfe/undo/main.cpp @@ -0,0 +1,16 @@ +//************************************************************************** +// Copyright (c) 2026 Autodesk, Inc. +// All rights reserved. +// +// Use of this software is subject to the terms of the Autodesk license +// agreement provided at the time of installation or download, or which +// otherwise accompanies this software in either electronic or hard copy form. +//**************************************************************************/ + +#include + +int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/lib/usdUfe/undo/test_UsdUndoUtils.cpp b/test/lib/usdUfe/undo/test_UsdUndoUtils.cpp new file mode 100644 index 0000000000..ecd947c845 --- /dev/null +++ b/test/lib/usdUfe/undo/test_UsdUndoUtils.cpp @@ -0,0 +1,112 @@ +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace { + +UsdStageRefPtr makeStageWithRenderSettings() +{ + auto stage = UsdStage::CreateInMemory(); + UsdRenderSettings::Define(stage, SdfPath("/Render/Settings")); + return stage; +} + +GfVec2i getResolution(const UsdRenderSettings& settings) +{ + GfVec2i resolution; + EXPECT_TRUE(settings.GetResolutionAttr().Get(&resolution)); + return resolution; +} + +SdfPathVector getCameraTargets(const UsdRenderSettings& settings) +{ + SdfPathVector targets; + settings.GetCameraRel().GetTargets(&targets); + return targets; +} + +} // namespace + +TEST(UsdUndoUtils, trackStagesEditTargetsInstallsDelegate) +{ + auto stage = makeStageWithRenderSettings(); + UsdUfe::trackStagesEditTargets({ stage }); + + const auto delegate = TfDynamic_cast( + stage->GetEditTarget().GetLayer()->GetStateDelegate()); + ASSERT_TRUE(delegate); +} + +TEST(UsdUndoUtils, trackStagesEditTargetsIsIdempotent) +{ + auto stage = makeStageWithRenderSettings(); + UsdUfe::trackStagesEditTargets({ stage }); + + const auto delegateBefore = TfDynamic_cast( + stage->GetEditTarget().GetLayer()->GetStateDelegate()); + ASSERT_TRUE(delegateBefore); + + UsdUfe::trackStagesEditTargets({ stage }); + + const auto delegateAfter = TfDynamic_cast( + stage->GetEditTarget().GetLayer()->GetStateDelegate()); + EXPECT_EQ(delegateBefore, delegateAfter); +} + +TEST(UsdLabeledEditUndoableCommand, attributeEditRoundtrip) +{ + auto stage = makeStageWithRenderSettings(); + UsdUfe::trackStagesEditTargets({ stage }); + + auto settings = UsdRenderSettings(stage->GetPrimAtPath(SdfPath("/Render/Settings"))); + ASSERT_TRUE(settings); + + auto cmd + = std::make_shared("Edit resolution", [&settings]() { + settings.GetResolutionAttr().Set(GfVec2i(1920, 1080)); + }); + cmd->execute(); + + EXPECT_EQ(getResolution(settings), GfVec2i(1920, 1080)); + + cmd->undo(); + EXPECT_FALSE(settings.GetResolutionAttr().HasAuthoredValue()); + + cmd->redo(); + EXPECT_EQ(getResolution(settings), GfVec2i(1920, 1080)); +} + +TEST(UsdLabeledEditUndoableCommand, relationshipEditRoundtrip) +{ + auto stage = makeStageWithRenderSettings(); + UsdUfe::trackStagesEditTargets({ stage }); + + auto settings = UsdRenderSettings(stage->GetPrimAtPath(SdfPath("/Render/Settings"))); + ASSERT_TRUE(settings); + + const SdfPath cameraPath("/Camera"); + stage->DefinePrim(cameraPath, TfToken("Camera")); + + auto cmd = std::make_shared( + "Edit camera", + [&settings, cameraPath]() { settings.GetCameraRel().SetTargets({ cameraPath }); }); + cmd->execute(); + + EXPECT_EQ(getCameraTargets(settings), SdfPathVector({ cameraPath })); + + cmd->undo(); + EXPECT_FALSE(settings.GetCameraRel().HasAuthoredTargets()); + + cmd->redo(); + EXPECT_EQ(getCameraTargets(settings), SdfPathVector({ cameraPath })); +} From c70d9d1ac5d8e1f9cd93270632747914a99ea60b Mon Sep 17 00:00:00 2001 From: Kevin Derler Date: Thu, 18 Jun 2026 09:01:46 -0400 Subject: [PATCH 2/7] SdfUndo block --- lib/usd/ui/renderSetup/MayaEditCommitter.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/usd/ui/renderSetup/MayaEditCommitter.cpp b/lib/usd/ui/renderSetup/MayaEditCommitter.cpp index f92d444a21..9d14609b4f 100644 --- a/lib/usd/ui/renderSetup/MayaEditCommitter.cpp +++ b/lib/usd/ui/renderSetup/MayaEditCommitter.cpp @@ -20,6 +20,8 @@ #include +#include + #include #include @@ -96,7 +98,10 @@ void MayaEditCommitter::commit(const std::string& undoLabel, std::function Date: Thu, 18 Jun 2026 09:26:12 -0400 Subject: [PATCH 3/7] Remove ambigious tests that did not actually cover the code paths. --- test/lib/CMakeLists.txt | 1 - test/lib/mayaUsd/CMakeLists.txt | 3 - test/lib/mayaUsd/ui/CMakeLists.txt | 18 --- test/lib/mayaUsd/ui/testRenderSetupUndo.py | 137 --------------------- test/lib/usdUfe/CMakeLists.txt | 1 - test/lib/usdUfe/undo/CMakeLists.txt | 36 ------ test/lib/usdUfe/undo/main.cpp | 16 --- test/lib/usdUfe/undo/test_UsdUndoUtils.cpp | 112 ----------------- 8 files changed, 324 deletions(-) delete mode 100644 test/lib/mayaUsd/ui/CMakeLists.txt delete mode 100644 test/lib/mayaUsd/ui/testRenderSetupUndo.py delete mode 100644 test/lib/usdUfe/CMakeLists.txt delete mode 100644 test/lib/usdUfe/undo/CMakeLists.txt delete mode 100644 test/lib/usdUfe/undo/main.cpp delete mode 100644 test/lib/usdUfe/undo/test_UsdUndoUtils.cpp diff --git a/test/lib/CMakeLists.txt b/test/lib/CMakeLists.txt index e731ac8173..b92f0a3f75 100644 --- a/test/lib/CMakeLists.txt +++ b/test/lib/CMakeLists.txt @@ -203,7 +203,6 @@ endforeach() add_subdirectory(ufe) add_subdirectory(mayaUsd) add_subdirectory(usd) -add_subdirectory(usdUfe) if (BUILD_MAYAUSDAPI_LIBRARY) add_subdirectory(mayaUsdAPI) endif() diff --git a/test/lib/mayaUsd/CMakeLists.txt b/test/lib/mayaUsd/CMakeLists.txt index 733e33601b..7b9fc51d3c 100644 --- a/test/lib/mayaUsd/CMakeLists.txt +++ b/test/lib/mayaUsd/CMakeLists.txt @@ -3,6 +3,3 @@ add_subdirectory(nodes) add_subdirectory(render) add_subdirectory(undo) add_subdirectory(utils) -if (AdskUsdRenderSetup_FOUND) - add_subdirectory(ui) -endif() diff --git a/test/lib/mayaUsd/ui/CMakeLists.txt b/test/lib/mayaUsd/ui/CMakeLists.txt deleted file mode 100644 index 25d6a9bfd6..0000000000 --- a/test/lib/mayaUsd/ui/CMakeLists.txt +++ /dev/null @@ -1,18 +0,0 @@ -if (AdskUsdRenderSetup_FOUND) - set(TEST_SCRIPT_FILES - testRenderSetupUndo.py - ) - - foreach(script ${TEST_SCRIPT_FILES}) - mayaUsd_get_unittest_target(target ${script}) - mayaUsd_add_test(${target} - PYTHON_MODULE ${target} - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - ENV - "LD_LIBRARY_PATH=${ADDITIONAL_LD_LIBRARY_PATH}" - ) - - set_property(TEST ${target} APPEND PROPERTY LABELS undo) - set_property(TEST ${target} APPEND PROPERTY LABELS SharedComponents) - endforeach() -endif() diff --git a/test/lib/mayaUsd/ui/testRenderSetupUndo.py b/test/lib/mayaUsd/ui/testRenderSetupUndo.py deleted file mode 100644 index 5b4b73c1d8..0000000000 --- a/test/lib/mayaUsd/ui/testRenderSetupUndo.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env python - -# -# Copyright 2026 Autodesk -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import unittest - -import maya.cmds as cmds -import mayaUsd_createStageWithNewLayer - -from pxr import Gf, Sdf, UsdGeom, UsdRender - -import mayaUsd.lib as mayaUsdLib -from mayaUsd.lib import UsdDefaultRenderSettings - - -def _authorRenderSettings(stage): - '''Author a minimal render-settings graph on stage.''' - settings = UsdRender.Settings.Define(stage, Sdf.Path('/Render/Settings')) - UsdGeom.Camera.Define(stage, Sdf.Path('/Camera')) - return settings - - -def _mayaEditCommit(stage, doEdit, undoLabel='Edit'): - '''Protocol-level simulation of MayaEditCommitter::commit() for testing. - - Exercises the USD undo-capture flow (trackLayerStates + UsdUndoBlock wrapped - in a Maya undo chunk) without driving the C++ class directly, since - MayaEditCommitter has no Python bindings. Tests that use this helper cover - the undo/redo correctness of the capture protocol; they do not exercise - MayaEditCommitter-specific behaviour such as chunk-name sanitization or the - isLocalEditInFlight counter. - ''' - mayaUsdLib.UsdUndoManager.trackLayerStates(stage.GetEditTarget().GetLayer()) - chunkName = undoLabel.replace(' ', '_') - cmds.undoInfo(openChunk=True, chunkName=chunkName) - try: - with mayaUsdLib.UsdUndoBlock(): - doEdit() - finally: - cmds.undoInfo(closeChunk=True) - - -class TestRenderSetupUndo(unittest.TestCase): - '''Undo/redo for Render Setup-style USD edits on proxy-shape stages. - - Tests use _mayaEditCommit() to simulate the capture protocol. Full - integration coverage of MayaEditCommitter::commit() (chunk-name escaping, - isLocalEditInFlight gating) requires C++ test infrastructure. - ''' - - @classmethod - def setUpClass(cls): - cmds.loadPlugin('mayaUsdPlugin') - - def setUp(self): - cmds.file(force=True, new=True) - cmds.select(clear=True) - - def _createProxyStage(self): - shapeNode = mayaUsd_createStageWithNewLayer.createStageWithNewLayer() - stage = mayaUsdLib.GetPrim(shapeNode).GetStage() - settings = _authorRenderSettings(stage) - return stage, settings - - def testAttributeEditUndoRedo(self): - stage, settings = self._createProxyStage() - nbCmds = cmds.undoInfo(q=True) - - _mayaEditCommit(stage, lambda: settings.GetResolutionAttr().Set(Gf.Vec2i(1280, 720))) - - self.assertEqual(settings.GetResolutionAttr().Get(), Gf.Vec2i(1280, 720)) - self.assertEqual(cmds.undoInfo(q=True), nbCmds + 1) - - cmds.undo() - self.assertFalse(settings.GetResolutionAttr().HasAuthoredValue()) - - cmds.redo() - self.assertEqual(settings.GetResolutionAttr().Get(), Gf.Vec2i(1280, 720)) - - def testCameraRelationshipUndoRedo(self): - stage, settings = self._createProxyStage() - cameraPath = Sdf.Path('/Camera') - - _mayaEditCommit(stage, lambda: settings.GetCameraRel().SetTargets([cameraPath])) - self.assertEqual(settings.GetCameraRel().GetTargets(), [cameraPath]) - - cmds.undo() - self.assertEqual(settings.GetCameraRel().GetTargets(), []) - - cmds.redo() - self.assertEqual(settings.GetCameraRel().GetTargets(), [cameraPath]) - - @unittest.skipUnless( - hasattr(UsdDefaultRenderSettings, 'getUsdStage'), - 'Scene Render Settings stage requires MAYA_HAS_USD_SETTINGS_NODES') - def testSceneRenderSettingsUntrackedEditNotOnMayaUndoQueue(self): - '''A direct USD edit on the Scene Render Settings stage made without first - calling trackLayerStates is not captured and does not push a Maya undo entry. - - This tests the untracked path. When MayaEditCommitter is used and the - settings stage is included in setStages(), trackStagesEditTargets() installs - the delegate and the edit IS Maya-undoable. That path is blocked today - because UsdSceneSettingsManager does not yet feed the settings stage into - the Render Setup host-stage list (see Known limitation in README-USD-Undo.md). - ''' - stage = UsdDefaultRenderSettings.getUsdStage() - settings = UsdDefaultRenderSettings.getDefaultRenderSettingsPrim() - self.assertTrue(settings.IsValid()) - renderSettings = UsdRender.Settings(settings) - - nbCmds = cmds.undoInfo(q=True) - - # Edit without tracking — no delegate is installed, so nothing is captured. - with mayaUsdLib.UsdUndoBlock(): - renderSettings.GetResolutionAttr().Set(Gf.Vec2i(640, 480)) - - self.assertEqual(renderSettings.GetResolutionAttr().Get(), Gf.Vec2i(640, 480)) - # No undo entry should have been pushed. - self.assertEqual(cmds.undoInfo(q=True), nbCmds) - - cmds.undo() - # Value unchanged because nothing was on the undo queue. - self.assertEqual(renderSettings.GetResolutionAttr().Get(), Gf.Vec2i(640, 480)) diff --git a/test/lib/usdUfe/CMakeLists.txt b/test/lib/usdUfe/CMakeLists.txt deleted file mode 100644 index 5b9290cdf1..0000000000 --- a/test/lib/usdUfe/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -add_subdirectory(undo) diff --git a/test/lib/usdUfe/undo/CMakeLists.txt b/test/lib/usdUfe/undo/CMakeLists.txt deleted file mode 100644 index 4f5f9138c7..0000000000 --- a/test/lib/usdUfe/undo/CMakeLists.txt +++ /dev/null @@ -1,36 +0,0 @@ -function(add_mayaUsdUfeUndo_test TARGET_NAME) - add_executable(${TARGET_NAME}) - - target_sources(${TARGET_NAME} - PRIVATE - main.cpp - ${ARGN} - ) - - mayaUsd_compile_config(${TARGET_NAME}) - - target_compile_definitions(${TARGET_NAME} - PRIVATE - $<$:TBB_USE_DEBUG> - $<$:BOOST_DEBUG_PYTHON> - $<$:BOOST_LINKING_PYTHON> - ) - - target_link_libraries(${TARGET_NAME} - PRIVATE - GTest::GTest - usdUfe - usdRender - ) - - mayaUsd_add_test(${TARGET_NAME} - COMMAND $ - ENV - "LD_LIBRARY_PATH=${ADDITIONAL_LD_LIBRARY_PATH}" - ) -endfunction() - -add_mayaUsdUfeUndo_test( - testUsdUfeUndo - test_UsdUndoUtils.cpp -) diff --git a/test/lib/usdUfe/undo/main.cpp b/test/lib/usdUfe/undo/main.cpp deleted file mode 100644 index a5ce5953b2..0000000000 --- a/test/lib/usdUfe/undo/main.cpp +++ /dev/null @@ -1,16 +0,0 @@ -//************************************************************************** -// Copyright (c) 2026 Autodesk, Inc. -// All rights reserved. -// -// Use of this software is subject to the terms of the Autodesk license -// agreement provided at the time of installation or download, or which -// otherwise accompanies this software in either electronic or hard copy form. -//**************************************************************************/ - -#include - -int main(int argc, char** argv) -{ - testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/lib/usdUfe/undo/test_UsdUndoUtils.cpp b/test/lib/usdUfe/undo/test_UsdUndoUtils.cpp deleted file mode 100644 index ecd947c845..0000000000 --- a/test/lib/usdUfe/undo/test_UsdUndoUtils.cpp +++ /dev/null @@ -1,112 +0,0 @@ -#include -#include -#include - -#include -#include -#include -#include -#include - -#include - -PXR_NAMESPACE_USING_DIRECTIVE - -namespace { - -UsdStageRefPtr makeStageWithRenderSettings() -{ - auto stage = UsdStage::CreateInMemory(); - UsdRenderSettings::Define(stage, SdfPath("/Render/Settings")); - return stage; -} - -GfVec2i getResolution(const UsdRenderSettings& settings) -{ - GfVec2i resolution; - EXPECT_TRUE(settings.GetResolutionAttr().Get(&resolution)); - return resolution; -} - -SdfPathVector getCameraTargets(const UsdRenderSettings& settings) -{ - SdfPathVector targets; - settings.GetCameraRel().GetTargets(&targets); - return targets; -} - -} // namespace - -TEST(UsdUndoUtils, trackStagesEditTargetsInstallsDelegate) -{ - auto stage = makeStageWithRenderSettings(); - UsdUfe::trackStagesEditTargets({ stage }); - - const auto delegate = TfDynamic_cast( - stage->GetEditTarget().GetLayer()->GetStateDelegate()); - ASSERT_TRUE(delegate); -} - -TEST(UsdUndoUtils, trackStagesEditTargetsIsIdempotent) -{ - auto stage = makeStageWithRenderSettings(); - UsdUfe::trackStagesEditTargets({ stage }); - - const auto delegateBefore = TfDynamic_cast( - stage->GetEditTarget().GetLayer()->GetStateDelegate()); - ASSERT_TRUE(delegateBefore); - - UsdUfe::trackStagesEditTargets({ stage }); - - const auto delegateAfter = TfDynamic_cast( - stage->GetEditTarget().GetLayer()->GetStateDelegate()); - EXPECT_EQ(delegateBefore, delegateAfter); -} - -TEST(UsdLabeledEditUndoableCommand, attributeEditRoundtrip) -{ - auto stage = makeStageWithRenderSettings(); - UsdUfe::trackStagesEditTargets({ stage }); - - auto settings = UsdRenderSettings(stage->GetPrimAtPath(SdfPath("/Render/Settings"))); - ASSERT_TRUE(settings); - - auto cmd - = std::make_shared("Edit resolution", [&settings]() { - settings.GetResolutionAttr().Set(GfVec2i(1920, 1080)); - }); - cmd->execute(); - - EXPECT_EQ(getResolution(settings), GfVec2i(1920, 1080)); - - cmd->undo(); - EXPECT_FALSE(settings.GetResolutionAttr().HasAuthoredValue()); - - cmd->redo(); - EXPECT_EQ(getResolution(settings), GfVec2i(1920, 1080)); -} - -TEST(UsdLabeledEditUndoableCommand, relationshipEditRoundtrip) -{ - auto stage = makeStageWithRenderSettings(); - UsdUfe::trackStagesEditTargets({ stage }); - - auto settings = UsdRenderSettings(stage->GetPrimAtPath(SdfPath("/Render/Settings"))); - ASSERT_TRUE(settings); - - const SdfPath cameraPath("/Camera"); - stage->DefinePrim(cameraPath, TfToken("Camera")); - - auto cmd = std::make_shared( - "Edit camera", - [&settings, cameraPath]() { settings.GetCameraRel().SetTargets({ cameraPath }); }); - cmd->execute(); - - EXPECT_EQ(getCameraTargets(settings), SdfPathVector({ cameraPath })); - - cmd->undo(); - EXPECT_FALSE(settings.GetCameraRel().HasAuthoredTargets()); - - cmd->redo(); - EXPECT_EQ(getCameraTargets(settings), SdfPathVector({ cameraPath })); -} From a50ae43e56863555ba40fdd49126c72d8817f02b Mon Sep 17 00:00:00 2001 From: Kevin Derler Date: Thu, 18 Jun 2026 16:20:52 -0400 Subject: [PATCH 4/7] Review changes : Fix cmakes. Fix filenames Fix member names Refactor UndoChunkUtils --- .../ui/debugTools/CompositionEditorCmd.cpp | 31 ++------- lib/usd/ui/layerEditor/mayaCommandHook.cpp | 8 +-- lib/usd/ui/renderSetup/CMakeLists.txt | 5 +- lib/usd/ui/renderSetup/MayaEditCommitter.cpp | 59 ++++------------- lib/usd/ui/renderSetup/MayaEditCommitter.h | 12 ++-- .../ui/renderSetup/renderSetupWindowCmd.cpp | 2 +- lib/usd/ui/undoChunkUtils.h | 66 +++++++++++++++++++ lib/usdUfe/undo/UsdUndoUtils.cpp | 4 +- 8 files changed, 97 insertions(+), 90 deletions(-) create mode 100644 lib/usd/ui/undoChunkUtils.h diff --git a/lib/usd/ui/debugTools/CompositionEditorCmd.cpp b/lib/usd/ui/debugTools/CompositionEditorCmd.cpp index 680bb636a1..4f53ad5948 100644 --- a/lib/usd/ui/debugTools/CompositionEditorCmd.cpp +++ b/lib/usd/ui/debugTools/CompositionEditorCmd.cpp @@ -15,6 +15,8 @@ // #include "CompositionEditorCmd.h" +#include "../undoChunkUtils.h" + #include #include @@ -62,31 +64,6 @@ constexpr auto kReloadFlagLong = "-reload"; const MString WORKSPACE_CONTROL_NAME = "mayaUsdCompositionEditor"; -// RAII guard that opens a named Maya undo chunk -class UndoChunkContext -{ -private: - // Maya's undo chunk names cannot contain spaces (the name is split at the - // first space), so replace them with underscores before quoting. - MString cleanChunkName(const std::string& label) - { - std::string name = label.empty() ? "USD Composition Edit" : label; - std::replace(name.begin(), name.end(), ' ', '_'); - return MString("\"") + name.c_str() + "\""; - } - -public: - explicit UndoChunkContext(const std::string& label) - { - MGlobal::executeCommand( - MString("undoInfo -openChunk -chunkName ") + cleanChunkName(label), false, false); - } - ~UndoChunkContext() { MGlobal::executeCommand("undoInfo -closeChunk", false, false); } - - UndoChunkContext(const UndoChunkContext&) = delete; - UndoChunkContext& operator=(const UndoChunkContext&) = delete; -}; - QPointer g_compositionEditorWidget; Ufe::Observer::Ptr g_selectionObserver; @@ -167,8 +144,8 @@ class MayaCompositionEditorHost : public Adsk::UsdDebug::ApplicationHost UsdUfe::UsdUndoManager::instance().trackLayerStates(layer); } - UndoChunkContext undoChunk(editLabel); - MayaUsdUndoBlock undoBlock; + MayaUsdUI::UndoChunkGuard undoChunk(editLabel); + MayaUsdUndoBlock undoBlock; return edit(); } diff --git a/lib/usd/ui/layerEditor/mayaCommandHook.cpp b/lib/usd/ui/layerEditor/mayaCommandHook.cpp index a0fd4b9b66..71bc0c979f 100644 --- a/lib/usd/ui/layerEditor/mayaCommandHook.cpp +++ b/lib/usd/ui/layerEditor/mayaCommandHook.cpp @@ -16,6 +16,7 @@ #include "mayaCommandHook.h" +#include "../undoChunkUtils.h" #include "abstractCommandHook.h" #include "mayaSessionState.h" @@ -46,9 +47,6 @@ namespace { std::string quote(const std::string& string) { return STR(" \"") + string + STR("\""); } -// maya doesn't support spaces in undo chunk names... -MString cleanChunkName(QString name) { return quote(name.replace(" ", "_").toStdString()).c_str(); } - std::string getProxyShapeName(const std::string& proxyShapePath) { std::size_t found = proxyShapePath.find_last_of("|"); @@ -102,7 +100,9 @@ void MayaCommandHook::setEditTarget(UsdLayer usdLayer) void MayaCommandHook::openUndoBracket(const QString& name) { MGlobal::executeCommand( - MString("undoInfo -openChunk -chunkName ") + cleanChunkName(name), false, false); + MString("undoInfo -openChunk -chunkName ") + MayaUsdUI::cleanChunkName(name.toStdString()), + false, + false); } // closes a complex undo operation in the host app. Please use UndoContext class to safely diff --git a/lib/usd/ui/renderSetup/CMakeLists.txt b/lib/usd/ui/renderSetup/CMakeLists.txt index 759f5f59d0..b5b668ab50 100644 --- a/lib/usd/ui/renderSetup/CMakeLists.txt +++ b/lib/usd/ui/renderSetup/CMakeLists.txt @@ -17,14 +17,13 @@ target_sources(${PROJECT_NAME} PRIVATE - MayaEditCommitter.cpp - MayaEditCommitter.h + mayaEditCommitter.cpp renderSetupWindowCmd.cpp ) mayaUsd_promoteHeaderList( HEADERS - MayaEditCommitter.h + mayaEditCommitter.h renderSetupWindowCmd.h BASEDIR ${PROJECT_NAME}/ui diff --git a/lib/usd/ui/renderSetup/MayaEditCommitter.cpp b/lib/usd/ui/renderSetup/MayaEditCommitter.cpp index 9d14609b4f..98ea9d6ec1 100644 --- a/lib/usd/ui/renderSetup/MayaEditCommitter.cpp +++ b/lib/usd/ui/renderSetup/MayaEditCommitter.cpp @@ -14,7 +14,9 @@ // limitations under the License. // -#include "MayaEditCommitter.h" +#include "mayaEditCommitter.h" + +#include "../undoChunkUtils.h" #include @@ -22,42 +24,8 @@ #include -#include -#include - -#include #include -namespace { - -// Maya does not support spaces in undo chunk names. Backslash and double-quote -// are escaped so the label is safe to embed in a MEL double-quoted string. -MString cleanChunkName(const std::string& label) -{ - QString name = QString::fromStdString(label); - name.replace(QLatin1Char('\\'), QLatin1String("\\\\")); - name.replace(QLatin1Char('"'), QLatin1String("\\\"")); - name.replace(QLatin1Char(' '), QLatin1Char('_')); - return MString(("\"" + name.toStdString() + "\"").c_str()); -} - -void openUndoChunk(const std::string& label) -{ - if (label.empty()) { - MGlobal::executeCommand("undoInfo -openChunk", false, false); - } else { - MGlobal::executeCommand( - MString("undoInfo -openChunk -chunkName ") + cleanChunkName(label), false, false); - } -} - -struct UndoChunkGuard -{ - ~UndoChunkGuard() { MGlobal::executeCommand("undoInfo -closeChunk", false, false); } -}; - -} // namespace - namespace MayaUsdRenderSetup { MayaEditCommitter::MayaEditCommitter(QObject* parent) @@ -67,37 +35,36 @@ MayaEditCommitter::MayaEditCommitter(QObject* parent) void MayaEditCommitter::setStages(const std::vector& stages) { - m_stages.clear(); - m_stages.reserve(stages.size()); + _stages.clear(); + _stages.reserve(stages.size()); for (const auto& hostStage : stages) { if (hostStage.stage) { - m_stages.push_back(hostStage.stage); + _stages.push_back(hostStage.stage); } } } void MayaEditCommitter::commit(const std::string& undoLabel, std::function doEdit) { - if (!doEdit || m_stages.empty()) { + if (!doEdit || _stages.empty()) { return; } - ++m_inFlightCount; + ++_inFlightCount; // Schedule the decrement before doEdit() so it fires even if doEdit() throws. // The one-event-loop-cycle delay keeps isLocalEditInFlight() true through the // USD-notice burst that follows the edit, so the notice bridge treats the // resulting refresh as self-originated and skips re-entrancy. QTimer::singleShot(0, this, [this]() { - if (m_inFlightCount > 0) { - --m_inFlightCount; + if (_inFlightCount > 0) { + --_inFlightCount; } }); - UsdUfe::trackStagesEditTargets(m_stages); + UsdUfe::trackStagesEditTargets(_stages); - openUndoChunk(undoLabel); - const UndoChunkGuard undoChunkGuard; - MayaUsd::MayaUsdUndoBlock block; + const MayaUsdUI::UndoChunkGuard undoChunkGuard(undoLabel); + MayaUsd::MayaUsdUndoBlock block; { PXR_NS::SdfChangeBlock changeBlock; doEdit(); diff --git a/lib/usd/ui/renderSetup/MayaEditCommitter.h b/lib/usd/ui/renderSetup/MayaEditCommitter.h index 3febb0a792..2dae20ac47 100644 --- a/lib/usd/ui/renderSetup/MayaEditCommitter.h +++ b/lib/usd/ui/renderSetup/MayaEditCommitter.h @@ -14,8 +14,8 @@ // limitations under the License. // -#ifndef MAYAUSD_UI_RENDERSETUP_MAYAEDITCOMMITTER_H -#define MAYAUSD_UI_RENDERSETUP_MAYAEDITCOMMITTER_H +#ifndef MAYAUSDUI_USD_RENDERSETUP_MAYAEDITCOMMITTER_H +#define MAYAUSDUI_USD_RENDERSETUP_MAYAEDITCOMMITTER_H #include @@ -43,13 +43,13 @@ class MayaEditCommitter void setStages(const std::vector& stages); void commit(const std::string& undoLabel, std::function doEdit) override; - bool isLocalEditInFlight() const override { return m_inFlightCount > 0; } + bool isLocalEditInFlight() const override { return _inFlightCount > 0; } private: - std::vector m_stages; - int m_inFlightCount { 0 }; + std::vector _stages; + int _inFlightCount { 0 }; }; } // namespace MayaUsdRenderSetup -#endif // MAYAUSD_UI_RENDERSETUP_MAYAEDITCOMMITTER_H +#endif // MAYAUSDUI_USD_RENDERSETUP_MAYAEDITCOMMITTER_H diff --git a/lib/usd/ui/renderSetup/renderSetupWindowCmd.cpp b/lib/usd/ui/renderSetup/renderSetupWindowCmd.cpp index ec34449ea6..afec1b21ec 100644 --- a/lib/usd/ui/renderSetup/renderSetupWindowCmd.cpp +++ b/lib/usd/ui/renderSetup/renderSetupWindowCmd.cpp @@ -15,7 +15,7 @@ // #include "renderSetupWindowCmd.h" -#include "MayaEditCommitter.h" +#include "mayaEditCommitter.h" #include #include diff --git a/lib/usd/ui/undoChunkUtils.h b/lib/usd/ui/undoChunkUtils.h new file mode 100644 index 0000000000..785bade4e7 --- /dev/null +++ b/lib/usd/ui/undoChunkUtils.h @@ -0,0 +1,66 @@ +// +// Copyright 2026 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#ifndef MAYAUSDUI_UNDO_CHUNK_UTILS_H +#define MAYAUSDUI_UNDO_CHUNK_UTILS_H + +#include +#include + +#include +#include + +#include + +namespace MayaUsdUI { + +//! Sanitizes a user-facing undo label for use as a Maya undo chunk name. +//! Maya's -chunkName flag does not accept spaces; backslash and double-quote +//! are escaped so the result is safe to embed in a MEL double-quoted string. +inline MString cleanChunkName(const std::string& label) +{ + QString name = QString::fromStdString(label); + name.replace(QLatin1Char('\\'), QLatin1String("\\\\")); + name.replace(QLatin1Char('"'), QLatin1String("\\\"")); + name.replace(QLatin1Char(' '), QLatin1Char('_')); + return MString(("\"" + name.toStdString() + "\"").c_str()); +} + +//! RAII guard that opens a named Maya undo chunk on construction and closes it +//! on destruction, ensuring the chunk is always balanced even if an exception +//! is thrown inside the guarded scope. +struct UndoChunkGuard +{ + //! Opens the undo chunk. If \p label is empty, no chunk name is set. + explicit UndoChunkGuard(const std::string& label) + { + if (label.empty()) { + MGlobal::executeCommand("undoInfo -openChunk", false, false); + } else { + MGlobal::executeCommand( + MString("undoInfo -openChunk -chunkName ") + cleanChunkName(label), false, false); + } + } + + ~UndoChunkGuard() { MGlobal::executeCommand("undoInfo -closeChunk", false, false); } + + UndoChunkGuard(const UndoChunkGuard&) = delete; + UndoChunkGuard& operator=(const UndoChunkGuard&) = delete; +}; + +} // namespace MayaUsdUI + +#endif // MAYAUSDUI_UNDO_CHUNK_UTILS_H diff --git a/lib/usdUfe/undo/UsdUndoUtils.cpp b/lib/usdUfe/undo/UsdUndoUtils.cpp index af93a942c9..438885f0f2 100644 --- a/lib/usdUfe/undo/UsdUndoUtils.cpp +++ b/lib/usdUfe/undo/UsdUndoUtils.cpp @@ -18,11 +18,9 @@ #include -PXR_NAMESPACE_USING_DIRECTIVE - namespace USDUFE_NS_DEF { -void trackStagesEditTargets(const std::vector& stages) +void trackStagesEditTargets(const std::vector& stages) { for (const auto& stage : stages) { if (!stage) { From e5db3c1aee1a8311b6c4d8339fed885d6ec20a10 Mon Sep 17 00:00:00 2001 From: Kevin Derler Date: Fri, 19 Jun 2026 09:28:24 -0400 Subject: [PATCH 5/7] Fix header --- lib/usd/ui/CMakeLists.txt | 1 + lib/usd/ui/debugTools/CompositionEditorCmd.cpp | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/usd/ui/CMakeLists.txt b/lib/usd/ui/CMakeLists.txt index d97fe217f1..307b7595a0 100644 --- a/lib/usd/ui/CMakeLists.txt +++ b/lib/usd/ui/CMakeLists.txt @@ -128,6 +128,7 @@ endif() set(HEADERS api.h initStringResources.h + undoChunkUtils.h ) mayaUsd_promoteHeaderList( diff --git a/lib/usd/ui/debugTools/CompositionEditorCmd.cpp b/lib/usd/ui/debugTools/CompositionEditorCmd.cpp index 4f53ad5948..3e2d4a148b 100644 --- a/lib/usd/ui/debugTools/CompositionEditorCmd.cpp +++ b/lib/usd/ui/debugTools/CompositionEditorCmd.cpp @@ -15,10 +15,9 @@ // #include "CompositionEditorCmd.h" -#include "../undoChunkUtils.h" - #include #include +#include #include From 088dacb611eaf932f1c8dab534001a5fe167f96a Mon Sep 17 00:00:00 2001 From: Kevin Derler Date: Fri, 19 Jun 2026 11:16:57 -0400 Subject: [PATCH 6/7] Files first letter to lowercase. --- .../renderSetup/{MayaEditCommitter.cpp => mayaEditCommitter.cpp} | 0 .../ui/renderSetup/{MayaEditCommitter.h => mayaEditCommitter.h} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename lib/usd/ui/renderSetup/{MayaEditCommitter.cpp => mayaEditCommitter.cpp} (100%) rename lib/usd/ui/renderSetup/{MayaEditCommitter.h => mayaEditCommitter.h} (100%) diff --git a/lib/usd/ui/renderSetup/MayaEditCommitter.cpp b/lib/usd/ui/renderSetup/mayaEditCommitter.cpp similarity index 100% rename from lib/usd/ui/renderSetup/MayaEditCommitter.cpp rename to lib/usd/ui/renderSetup/mayaEditCommitter.cpp diff --git a/lib/usd/ui/renderSetup/MayaEditCommitter.h b/lib/usd/ui/renderSetup/mayaEditCommitter.h similarity index 100% rename from lib/usd/ui/renderSetup/MayaEditCommitter.h rename to lib/usd/ui/renderSetup/mayaEditCommitter.h From b15985544556cb074cc6f718941bcb447b8b2337 Mon Sep 17 00:00:00 2001 From: Kevin Derler Date: Mon, 22 Jun 2026 10:20:09 -0400 Subject: [PATCH 7/7] Fix relative include path --- lib/usd/ui/layerEditor/mayaCommandHook.cpp | 2 +- lib/usd/ui/renderSetup/mayaEditCommitter.cpp | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/usd/ui/layerEditor/mayaCommandHook.cpp b/lib/usd/ui/layerEditor/mayaCommandHook.cpp index 71bc0c979f..b98e2c37f2 100644 --- a/lib/usd/ui/layerEditor/mayaCommandHook.cpp +++ b/lib/usd/ui/layerEditor/mayaCommandHook.cpp @@ -16,7 +16,6 @@ #include "mayaCommandHook.h" -#include "../undoChunkUtils.h" #include "abstractCommandHook.h" #include "mayaSessionState.h" @@ -24,6 +23,7 @@ #include #include #include +#include #include #include diff --git a/lib/usd/ui/renderSetup/mayaEditCommitter.cpp b/lib/usd/ui/renderSetup/mayaEditCommitter.cpp index 98ea9d6ec1..ba129034fd 100644 --- a/lib/usd/ui/renderSetup/mayaEditCommitter.cpp +++ b/lib/usd/ui/renderSetup/mayaEditCommitter.cpp @@ -16,9 +16,8 @@ #include "mayaEditCommitter.h" -#include "../undoChunkUtils.h" - #include +#include #include