├── test ├── requirements.txt ├── utility │ ├── plugins │ │ ├── CMakeLists.txt │ │ ├── newDispatcher │ │ │ ├── plugInfo.json │ │ │ ├── unfTest │ │ │ │ └── newDispatcher │ │ │ │ │ ├── dispatcher.cpp │ │ │ │ │ └── dispatcher.h │ │ │ └── CMakeLists.txt │ │ └── newStageDispatcher │ │ │ ├── plugInfo.json │ │ │ ├── unfTest │ │ │ └── newStageDispatcher │ │ │ │ ├── dispatcher.cpp │ │ │ │ └── dispatcher.h │ │ │ └── CMakeLists.txt │ ├── CMakeLists.txt │ └── unfTest │ │ ├── notice.cpp │ │ ├── observer.h │ │ ├── notice.h │ │ └── listener.h ├── unit │ ├── python │ │ ├── CMakeLists.txt │ │ ├── test_broker.py │ │ ├── test_layer_muting_changed.py │ │ └── test_objects_changed.py │ ├── testDispatcherPlugin.cpp │ ├── CMakeLists.txt │ ├── testBroker.cpp │ ├── testDispatcher.cpp │ └── testBrokerFlow.cpp ├── integration │ ├── python │ │ ├── CMakeLists.txt │ │ ├── conftest.py │ │ ├── test_change_edit_target.py │ │ ├── test_add_prims.py │ │ └── test_mute_layers.py │ ├── CMakeLists.txt │ ├── testChangeEditTarget.cpp │ └── testAddPrims.cpp └── CMakeLists.txt ├── doc ├── sphinx │ ├── favicon.png │ ├── api_reference │ │ ├── cpp.rst │ │ ├── index.rst │ │ └── python │ │ │ ├── index.rst │ │ │ ├── Notice │ │ │ ├── index.rst │ │ │ ├── StageEditTargetChanged.rst │ │ │ ├── StageContentsChanged.rst │ │ │ ├── StageNotice.rst │ │ │ ├── LayerMutingChanged.rst │ │ │ └── ObjectsChanged.rst │ │ │ ├── CapturePredicate.rst │ │ │ ├── NoticeTransaction.rst │ │ │ └── Broker.rst │ ├── release │ │ ├── index.rst │ │ └── migration_notes.rst │ ├── index.rst │ ├── environment_variables.rst │ ├── _static │ │ └── style.css │ ├── introduction.rst │ ├── conf.py │ ├── _extensions │ │ └── changelog.py │ ├── doxygen.py │ ├── glossary.rst │ ├── dispatchers.rst │ ├── installing.rst │ ├── getting_started.rst │ └── notices.rst ├── requirements.txt ├── doxygen │ ├── namespaces.dox │ ├── index.dox │ └── USD.tag └── CMakeLists.txt ├── cmake ├── unf-config.cmake.in └── modules │ ├── FindClangFormat.cmake │ └── FindUSD.cmake ├── src ├── python │ ├── __init__.py │ ├── module.cpp │ ├── wrapCapturePredicate.cpp │ ├── predicate.h │ ├── CMakeLists.txt │ ├── wrapBroker.cpp │ ├── wrapTransaction.cpp │ └── wrapNotice.cpp ├── unf │ ├── api.h │ ├── capturePredicate.cpp │ ├── transaction.cpp │ ├── dispatcher.cpp │ ├── capturePredicate.h │ ├── transaction.h │ ├── dispatcher.h │ ├── broker.cpp │ └── notice.cpp └── CMakeLists.txt ├── NOTICE.txt ├── etc └── roll │ └── unf.recipe ├── .versup.json ├── .clang-format ├── .gitignore ├── .github └── workflows │ ├── docs-deploy.yml │ ├── test-linux.yml │ └── test-windows.yml ├── README.md └── CMakeLists.txt /test/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest>=7 2 | pytest-cmake==0.* 3 | -------------------------------------------------------------------------------- /doc/sphinx/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdas/unf/HEAD/doc/sphinx/favicon.png -------------------------------------------------------------------------------- /cmake/unf-config.cmake.in: -------------------------------------------------------------------------------- 1 | @PACKAGE_INIT@ 2 | 3 | include(${CMAKE_CURRENT_LIST_DIR}/unf-targets.cmake) 4 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx==7.* 2 | sphinx-cmake==0.* 3 | sphinx-rtd-theme==2.* 4 | sphinxcontrib-doxylink==1.* -------------------------------------------------------------------------------- /src/python/__init__.py: -------------------------------------------------------------------------------- 1 | # :coding: utf-8 2 | 3 | from pxr import Tf 4 | Tf.PreparePythonModule() 5 | del Tf 6 | -------------------------------------------------------------------------------- /test/utility/plugins/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(newStageDispatcher) 2 | add_subdirectory(newDispatcher) 3 | -------------------------------------------------------------------------------- /doc/sphinx/api_reference/cpp.rst: -------------------------------------------------------------------------------- 1 | .. _api_reference/cpp: 2 | 3 | ******* 4 | C++ API 5 | ******* 6 | 7 | `Doxygen API <../doxygen/index.html>`_ 8 | -------------------------------------------------------------------------------- /doc/doxygen/namespaces.dox: -------------------------------------------------------------------------------- 1 | /*! 2 | \namespace unf 3 | Scope of the Usd Notice Framework library. 4 | 5 | \namespace unf::UnfNotice 6 | Regroup all standalone notices used by the library. 7 | */ 8 | -------------------------------------------------------------------------------- /doc/sphinx/api_reference/index.rst: -------------------------------------------------------------------------------- 1 | .. _api_reference: 2 | 3 | ************* 4 | API Reference 5 | ************* 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | 10 | cpp 11 | python/index 12 | -------------------------------------------------------------------------------- /doc/sphinx/api_reference/python/index.rst: -------------------------------------------------------------------------------- 1 | .. _api_reference/python: 2 | 3 | ********** 4 | Python API 5 | ********** 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | :glob: 10 | 11 | * 12 | */index 13 | -------------------------------------------------------------------------------- /src/python/module.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | TF_WRAP_MODULE 4 | { 5 | TF_WRAP(CapturePredicate); 6 | TF_WRAP(Broker); 7 | TF_WRAP(Notice); 8 | TF_WRAP(Transaction); 9 | } 10 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | USD Notice Framework 2 | Copyright 2023 Walt Disney Animation Studios 3 | 4 | All rights reserved. 5 | 6 | This product includes software developed at: 7 | 8 | Walt Disney Animation Studios 9 | (https://www.disneyanimation.com/). 10 | -------------------------------------------------------------------------------- /doc/sphinx/api_reference/python/Notice/index.rst: -------------------------------------------------------------------------------- 1 | ********** 2 | unf.Notice 3 | ********** 4 | 5 | .. py:class:: unf.Notice 6 | 7 | Regroup all standalone notices used by the library. 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | :glob: 12 | 13 | * 14 | -------------------------------------------------------------------------------- /doc/doxygen/index.dox: -------------------------------------------------------------------------------- 1 | /*! 2 | \mainpage Usd Notice Framework (Unf) 3 | 4 | Notice management library built over USD Notices. 5 | 6 | Read the [documentation](../index.html), or explore [the API](./annotated.html). 7 | 8 |
9 | 10 | Copyright 2023, Walt Disney Animation Studios 11 | */ 12 | -------------------------------------------------------------------------------- /test/unit/python/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | pytest_discover_tests( 2 | PythonUnitTests 3 | LIBRARY_PATH_PREPEND 4 | $ 5 | $ 6 | PYTHON_PATH_PREPEND 7 | "$/.." 8 | TRIM_FROM_NAME "^test_" 9 | BUNDLE_TESTS ${BUNDLE_PYTHON_TESTS} 10 | DEPENDS unf pyUnf 11 | ) 12 | -------------------------------------------------------------------------------- /test/integration/python/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | pytest_discover_tests( 2 | PythonIntegrationTest 3 | LIBRARY_PATH_PREPEND 4 | $ 5 | $ 6 | PYTHON_PATH_PREPEND 7 | "$/.." 8 | TRIM_FROM_NAME "^test_" 9 | BUNDLE_TESTS ${BUNDLE_PYTHON_TESTS} 10 | DEPENDS unf pyUnf 11 | ) 12 | -------------------------------------------------------------------------------- /doc/sphinx/release/index.rst: -------------------------------------------------------------------------------- 1 | .. _release: 2 | 3 | *************************** 4 | Release and migration notes 5 | *************************** 6 | 7 | Find out what has changed between versions and see important migration notes to 8 | be aware of when switching to a new version. 9 | 10 | .. toctree:: 11 | :maxdepth: 1 12 | 13 | release_notes 14 | migration_notes 15 | 16 | -------------------------------------------------------------------------------- /test/utility/plugins/newDispatcher/plugInfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "Plugins": [ 3 | { 4 | "Info": { 5 | "Types": { 6 | "Test::NewDispatcher": { 7 | "bases": [ "unf::Dispatcher" ] 8 | } 9 | } 10 | }, 11 | "LibraryPath": "$", 12 | "Name": "NewDispatcher", 13 | "Type": "library" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /test/utility/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(unfTest 2 | unfTest/notice.cpp 3 | ) 4 | 5 | target_compile_definitions(unfTest 6 | PRIVATE 7 | UNF_EXPORTS=1 8 | ) 9 | 10 | target_include_directories(unfTest 11 | PUBLIC 12 | $ 13 | ) 14 | 15 | target_link_libraries(unfTest 16 | PUBLIC 17 | unf 18 | ) 19 | 20 | add_subdirectory(plugins) 21 | -------------------------------------------------------------------------------- /test/utility/plugins/newStageDispatcher/plugInfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "Plugins": [ 3 | { 4 | "Info": { 5 | "Types": { 6 | "Test::NewStageDispatcher": { 7 | "bases": [ "unf::Dispatcher" ] 8 | } 9 | } 10 | }, 11 | "LibraryPath": "$", 12 | "Name": "NewStageDispatcher", 13 | "Type": "library" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /doc/sphinx/index.rst: -------------------------------------------------------------------------------- 1 | .. _main: 2 | 3 | #################### 4 | USD Notice Framework 5 | #################### 6 | 7 | Notice management library built over :term:`USD` Notices. 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | 12 | introduction 13 | installing 14 | getting_started 15 | notices 16 | dispatchers 17 | environment_variables 18 | api_reference/index 19 | release/index 20 | glossary 21 | -------------------------------------------------------------------------------- /doc/sphinx/api_reference/python/Notice/StageEditTargetChanged.rst: -------------------------------------------------------------------------------- 1 | ********************************* 2 | unf.Notice.StageEditTargetChanged 3 | ********************************* 4 | 5 | .. py:class:: unf.Notice.StageEditTargetChanged 6 | 7 | Base: :py:class:`unf.Notice.StageNotice` 8 | 9 | Notice sent when a stage's EditTarget has changed. 10 | 11 | This notice type is the standalone equivalent of the 12 | :usd-cpp:`UsdNotice::StageEditTargetChanged` notice type. 13 | -------------------------------------------------------------------------------- /doc/sphinx/environment_variables.rst: -------------------------------------------------------------------------------- 1 | .. _environment_variables: 2 | 3 | ********************* 4 | Environment variables 5 | ********************* 6 | 7 | Environment variables directly defined or referenced by this package. 8 | 9 | .. envvar:: PXR_PLUGINPATH_NAME 10 | 11 | Environment variable used to locate :term:`USD` plugin paths. 12 | 13 | .. seealso:: 14 | 15 | `Building USD 16 | `_ 17 | -------------------------------------------------------------------------------- /test/utility/plugins/newDispatcher/unfTest/newDispatcher/dispatcher.cpp: -------------------------------------------------------------------------------- 1 | #include "dispatcher.h" 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include 8 | 9 | PXR_NAMESPACE_USING_DIRECTIVE 10 | 11 | TF_REGISTRY_FUNCTION(TfType) 12 | { 13 | unf::DispatcherDefine<::Test::NewDispatcher, unf::Dispatcher>(); 14 | } 15 | 16 | void ::Test::NewDispatcher::Register() 17 | { 18 | _Register<::Test::InputNotice, ::Test::OutputNotice2>(); 19 | } 20 | -------------------------------------------------------------------------------- /doc/sphinx/api_reference/python/Notice/StageContentsChanged.rst: -------------------------------------------------------------------------------- 1 | ******************************* 2 | unf.Notice.StageContentsChanged 3 | ******************************* 4 | 5 | .. py:class:: unf.Notice.StageContentsChanged 6 | 7 | Base: :py:class:`unf.Notice.StageNotice` 8 | 9 | Notice sent when the given Usd Stage's contents have changed in 10 | any way. 11 | 12 | This notice type is the standalone equivalent of the 13 | :usd-cpp:`UsdNotice::StageContentsChanged` notice type. 14 | -------------------------------------------------------------------------------- /etc/roll/unf.recipe: -------------------------------------------------------------------------------- 1 | { 2 | "install": [ 3 | "mkdir -p build/%(build_flavor)s", 4 | "sh -c 'cmake -B build/%(build_flavor)s -S . -D CMAKE_INSTALL_PREFIX=%(prefix)s -D GTest_ROOT=$RP_gtest -D TBB_INCLUDE_DIRS=$RP_TBB/include -D Boost_ROOT=$RP_boost_disney -D USD_INCLUDE_DIR=$RP_presto_usd/include'", 5 | "make -j%(jobs)s -C build/%(build_flavor)s install DESTDIR='%(DESTDIR)s'" 6 | ], 7 | "test": [ 8 | "make -j%(jobs)s -C build/%(build_flavor)s test" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/utility/plugins/newStageDispatcher/unfTest/newStageDispatcher/dispatcher.cpp: -------------------------------------------------------------------------------- 1 | #include "dispatcher.h" 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include 8 | 9 | PXR_NAMESPACE_USING_DIRECTIVE 10 | 11 | TF_REGISTRY_FUNCTION(TfType) 12 | { 13 | unf::DispatcherDefine(); 14 | } 15 | 16 | void ::Test::NewStageDispatcher::Register() 17 | { 18 | _Register<::Test::InputNotice, ::Test::OutputNotice1>(); 19 | } 20 | -------------------------------------------------------------------------------- /doc/sphinx/release/migration_notes.rst: -------------------------------------------------------------------------------- 1 | .. _release/migration: 2 | 3 | *************** 4 | Migration notes 5 | *************** 6 | 7 | This section will show more detailed information when relevant for switching to 8 | a new version, such as when upgrading involves backwards incompatibilities. 9 | 10 | .. _release/migration/0.4.0: 11 | 12 | Migrate to 0.4.0 13 | ================ 14 | 15 | The Python package has been renamed from :mod:`usd_notice_framework` to 16 | :mod:`unf` for consistency. 17 | 18 | .. seealso:: :ref:`api_reference/python` 19 | -------------------------------------------------------------------------------- /test/utility/plugins/newDispatcher/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(unfTestNewDispatcher SHARED 2 | unfTest/newDispatcher/dispatcher.cpp 3 | ) 4 | 5 | target_compile_definitions(unfTestNewDispatcher 6 | PRIVATE 7 | UNF_EXPORTS=1 8 | ) 9 | 10 | target_link_libraries(unfTestNewDispatcher 11 | PUBLIC 12 | unf 13 | unfTest 14 | ) 15 | 16 | target_include_directories(unfTestNewDispatcher 17 | PUBLIC 18 | $ 19 | ) 20 | 21 | file( 22 | GENERATE 23 | OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/plugInfo_$.json" 24 | INPUT "plugInfo.json" 25 | ) 26 | -------------------------------------------------------------------------------- /test/integration/python/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pxr import Usd, Sdf 4 | 5 | import pytest 6 | 7 | @pytest.fixture() 8 | def stage_with_layers(): 9 | """Create a Stage with initial layers.""" 10 | stage = Usd.Stage.CreateInMemory() 11 | root_layer = stage.GetRootLayer() 12 | 13 | layers_nb = 3 14 | 15 | layers = [] 16 | identifiers = [] 17 | 18 | while len(layers) < layers_nb: 19 | layer = Sdf.Layer.CreateAnonymous(".usda") 20 | layers.append(layer) 21 | identifiers.append(layer.identifier) 22 | 23 | root_layer.subLayerPaths = identifiers 24 | return stage 25 | -------------------------------------------------------------------------------- /doc/sphinx/api_reference/python/CapturePredicate.rst: -------------------------------------------------------------------------------- 1 | ******************** 2 | unf.CapturePredicate 3 | ******************** 4 | 5 | .. py:class:: unf.CapturePredicate 6 | 7 | Predicate functor which indicates whether a notice can be captured 8 | during a transaction. 9 | 10 | .. py:staticmethod:: Default() 11 | 12 | Create a predicate which return true for each notice type. 13 | 14 | :return: Instance of :class:`unf.CapturePredicate`. 15 | 16 | .. py:staticmethod:: BlockAll() 17 | 18 | Create a predicate which return false for each notice type. 19 | 20 | :return: Instance of :class:`unf.CapturePredicate`. 21 | -------------------------------------------------------------------------------- /test/utility/plugins/newStageDispatcher/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(unfTestNewStageDispatcher SHARED 2 | unfTest/newStageDispatcher/dispatcher.cpp 3 | ) 4 | 5 | target_compile_definitions(unfTestNewStageDispatcher 6 | PRIVATE 7 | UNF_EXPORTS=1 8 | ) 9 | 10 | target_link_libraries(unfTestNewStageDispatcher 11 | PUBLIC 12 | unf 13 | unfTest 14 | ) 15 | 16 | target_include_directories(unfTestNewStageDispatcher 17 | PUBLIC 18 | $ 19 | ) 20 | 21 | file( 22 | GENERATE 23 | OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/plugInfo_$.json" 24 | INPUT "plugInfo.json" 25 | ) 26 | -------------------------------------------------------------------------------- /.versup.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "CMakeLists.txt": [ 4 | [ 5 | " VERSION ([\\d\\.]+)", 6 | " VERSION [version]" 7 | ] 8 | ], 9 | "doc/sphinx/release/release_notes.rst": [ 10 | [ 11 | ".. release:: Upcoming", 12 | ".. release:: [version]\n :date: [version_date]" 13 | ] 14 | ] 15 | }, 16 | "changelog": { 17 | "enabled": false 18 | }, 19 | "commit": { 20 | "mainbranch": "main" 21 | }, 22 | "tag": { 23 | "enabled": true, 24 | "name": "[version]" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /doc/sphinx/api_reference/python/Notice/StageNotice.rst: -------------------------------------------------------------------------------- 1 | ********************** 2 | unf.Notice.StageNotice 3 | ********************** 4 | 5 | .. py:class:: unf.Notice.StageNotice 6 | 7 | Interface class for standalone Usd Stage notices. 8 | 9 | This notice type is the standalone equivalent of the 10 | :usd-cpp:`UsdNotice::StageNotice` notice type. 11 | 12 | .. py:method:: IsMergeable() 13 | 14 | Indicate whether notice from the same type can be consolidated 15 | during a transaction. 16 | 17 | :return: Boolean value. 18 | 19 | .. py:method:: GetTypeId() 20 | 21 | Return unique type identifier. 22 | 23 | :return: String value. 24 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: Google 3 | AlignAfterOpenBracket: AlwaysBreak 4 | AllowAllParametersOfDeclarationOnNextLine: true 5 | AccessModifierOffset: -2 6 | BinPackArguments: false 7 | BreakBeforeBinaryOperators: NonAssignment 8 | BreakBeforeBraces: Stroustrup 9 | FixNamespaceComments: false 10 | IndentWidth: '4' 11 | Language: Cpp 12 | MaxEmptyLinesToKeep: 2 13 | SpaceAfterControlStatementKeyword: true 14 | SpaceAfterCStyleCast: false 15 | SpaceBeforeAssignmentOperators: true 16 | SpaceInEmptyParentheses: false 17 | SpacesBeforeTrailingComments: 2 18 | SpacesInAngles: false 19 | SpacesInCStyleCastParentheses: false 20 | SpacesInParentheses: false 21 | SortIncludes: false 22 | UseTab: Never 23 | ... 24 | -------------------------------------------------------------------------------- /test/utility/plugins/newDispatcher/unfTest/newDispatcher/dispatcher.h: -------------------------------------------------------------------------------- 1 | #ifndef TEST_USD_NOTICE_FRAMEWORK_PLUGIN_NEW_DISPATCHER_H 2 | #define TEST_USD_NOTICE_FRAMEWORK_PLUGIN_NEW_DISPATCHER_H 3 | 4 | #include 5 | #include 6 | 7 | namespace Test { 8 | 9 | class NewDispatcher : public unf::Dispatcher { 10 | public: 11 | UNF_API NewDispatcher(const unf::BrokerWeakPtr& broker) 12 | : unf::Dispatcher(broker) 13 | { 14 | } 15 | 16 | UNF_API std::string GetIdentifier() const { return "NewDispatcher"; }; 17 | 18 | UNF_API void Register() override; 19 | }; 20 | 21 | } // namespace Test 22 | 23 | #endif // TEST_USD_NOTICE_FRAMEWORK_PLUGIN_NEW_DISPATCHER_H 24 | -------------------------------------------------------------------------------- /test/utility/plugins/newStageDispatcher/unfTest/newStageDispatcher/dispatcher.h: -------------------------------------------------------------------------------- 1 | #ifndef TEST_USD_NOTICE_FRAMEWORK_PLUGIN_NEW_STAGE_DISPATCHER_H 2 | #define TEST_USD_NOTICE_FRAMEWORK_PLUGIN_NEW_STAGE_DISPATCHER_H 3 | 4 | #include 5 | #include 6 | 7 | namespace Test { 8 | 9 | class NewStageDispatcher : public unf::Dispatcher { 10 | public: 11 | UNF_API NewStageDispatcher(const unf::BrokerWeakPtr& broker) 12 | : unf::Dispatcher(broker) 13 | { 14 | } 15 | 16 | UNF_API std::string GetIdentifier() const { return "StageDispatcher"; }; 17 | 18 | UNF_API void Register() override; 19 | }; 20 | 21 | } // namespace Test 22 | 23 | #endif // TEST_USD_NOTICE_FRAMEWORK_PLUGIN_NEW_STAGE_DISPATCHER_H 24 | -------------------------------------------------------------------------------- /doc/sphinx/api_reference/python/Notice/LayerMutingChanged.rst: -------------------------------------------------------------------------------- 1 | ***************************** 2 | unf.Notice.LayerMutingChanged 3 | ***************************** 4 | 5 | .. py:class:: unf.Notice.LayerMutingChanged 6 | 7 | Base: :py:class:`unf.Notice.StageNotice` 8 | 9 | Notice sent after a set of layers have been newly muted or unmuted. 10 | 11 | This notice type is the standalone equivalent of the 12 | :usd-cpp:`UsdNotice::LayerMutingChanged` notice type. 13 | 14 | .. py:method:: GetMutedLayers() 15 | 16 | Returns identifiers of the layers that were muted. 17 | 18 | :return: List of layer identifiers. 19 | 20 | .. py:method:: GetUnmutedLayers() 21 | 22 | Returns identifiers of the layers that were unmuted. 23 | 24 | :return: List of layer identifiers. 25 | -------------------------------------------------------------------------------- /src/unf/api.h: -------------------------------------------------------------------------------- 1 | #ifndef USD_NOTICE_FRAMEWORK_API_H 2 | #define USD_NOTICE_FRAMEWORK_API_H 3 | 4 | #include 5 | 6 | #if defined(UNF_STATIC) 7 | #define UNF_API 8 | #define UNF_API_TEMPLATE_CLASS(...) 9 | #define UNF_API_TEMPLATE_STRUCT(...) 10 | #define UNF_LOCAL 11 | #else 12 | #if defined(UNF_EXPORTS) 13 | #define UNF_API ARCH_EXPORT 14 | #define UNF_API_TEMPLATE_CLASS(...) ARCH_EXPORT_TEMPLATE(class, __VA_ARGS__) 15 | #define UNF_API_TEMPLATE_STRUCT(...) ARCH_EXPORT_TEMPLATE(struct, __VA_ARGS__) 16 | #else 17 | #define UNF_API ARCH_IMPORT 18 | #define UNF_API_TEMPLATE_CLASS(...) ARCH_IMPORT_TEMPLATE(class, __VA_ARGS__) 19 | #define UNF_API_TEMPLATE_STRUCT(...) ARCH_IMPORT_TEMPLATE(struct, __VA_ARGS__) 20 | #endif 21 | #define UNF_LOCAL ARCH_HIDDEN 22 | #endif 23 | 24 | #endif // USD_NOTICE_FRAMEWORK_API_H 25 | -------------------------------------------------------------------------------- /src/unf/capturePredicate.cpp: -------------------------------------------------------------------------------- 1 | #include "unf/capturePredicate.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace unf { 10 | 11 | CapturePredicate::CapturePredicate(const CapturePredicateFunc& function) 12 | : _function(function) 13 | { 14 | } 15 | 16 | bool CapturePredicate::operator()(const UnfNotice::StageNotice& notice) const 17 | { 18 | if (!_function) return true; 19 | return _function(notice); 20 | } 21 | 22 | CapturePredicate CapturePredicate::Default() 23 | { 24 | auto function = [](const UnfNotice::StageNotice&) { return true; }; 25 | return CapturePredicate(function); 26 | } 27 | 28 | CapturePredicate CapturePredicate::BlockAll() 29 | { 30 | auto function = [](const UnfNotice::StageNotice&) { return false; }; 31 | return CapturePredicate(function); 32 | } 33 | 34 | } // namespace unf 35 | -------------------------------------------------------------------------------- /test/integration/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(testIntegrationAddPrims testAddPrims.cpp) 2 | target_link_libraries(testIntegrationAddPrims 3 | PRIVATE 4 | unf 5 | unfTest 6 | GTest::gtest 7 | GTest::gtest_main 8 | ) 9 | gtest_discover_tests(testIntegrationAddPrims) 10 | 11 | add_executable(testIntegrationMuteLayers testMuteLayers.cpp) 12 | target_link_libraries(testIntegrationMuteLayers 13 | PRIVATE 14 | unf 15 | unfTest 16 | GTest::gtest 17 | GTest::gtest_main 18 | ) 19 | gtest_discover_tests(testIntegrationMuteLayers) 20 | 21 | add_executable(testIntegrationChangeEditTarget testChangeEditTarget.cpp) 22 | target_link_libraries(testIntegrationChangeEditTarget 23 | PRIVATE 24 | unf 25 | unfTest 26 | GTest::gtest 27 | GTest::gtest_main 28 | ) 29 | gtest_discover_tests(testIntegrationChangeEditTarget) 30 | 31 | if (BUILD_PYTHON_BINDINGS) 32 | add_subdirectory(python) 33 | endif() 34 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | if (WIN32) 2 | # Monkeypatch Gtest discovery function to extend environment so that 3 | # relevant DLLs can be found on Windows. We use 'set_tests_properties' 4 | # to prevent issue with escaped semi-colons when passing environment 5 | # to 'gtest_discover_tests'. 6 | macro(gtest_discover_tests NAME) 7 | gtest_add_tests(TARGET ${NAME} TEST_LIST tests) 8 | 9 | # Extract and sanitize 'PATH' environment variable. 10 | string(REPLACE ";" "\;" env_path "$ENV{PATH}") 11 | 12 | # Extract and sanitize environment passed to function. 13 | cmake_parse_arguments("" "" "" "PROPERTIES" ${ARGN}) 14 | cmake_parse_arguments("" "" "" "ENVIRONMENT" ${_PROPERTIES}) 15 | 16 | # Apply environment to newly added tests. 17 | set_tests_properties(${tests} 18 | PROPERTIES ENVIRONMENT 19 | "${_ENVIRONMENT};PATH=$,\\;>\;${env_path}" 20 | ) 21 | endmacro() 22 | endif() 23 | 24 | add_subdirectory(utility) 25 | add_subdirectory(unit) 26 | add_subdirectory(integration) 27 | -------------------------------------------------------------------------------- /src/python/wrapCapturePredicate.cpp: -------------------------------------------------------------------------------- 1 | // clang-format off 2 | 3 | #include "./predicate.h" 4 | 5 | #include "unf/capturePredicate.h" 6 | 7 | #ifndef PXR_USE_INTERNAL_BOOST_PYTHON 8 | #include 9 | using namespace boost::python; 10 | #else 11 | #include 12 | using namespace PXR_BOOST_PYTHON_NAMESPACE; 13 | #endif 14 | 15 | using namespace unf; 16 | 17 | PXR_NAMESPACE_USING_DIRECTIVE 18 | 19 | 20 | void wrapCapturePredicate() 21 | { 22 | class_( 23 | "CapturePredicate", 24 | "Predicate functor which indicates whether a notice can be captured " 25 | "during a transaction.", 26 | no_init) 27 | 28 | .def( 29 | "Default", 30 | &CapturePredicate::Default, 31 | "Create a predicate which return true for each notice type.") 32 | .staticmethod("Default") 33 | 34 | .def( 35 | "BlockAll", 36 | &CapturePredicate::BlockAll, 37 | "Create a predicate which return false for each notice type.") 38 | .staticmethod("BlockAll"); 39 | } 40 | -------------------------------------------------------------------------------- /src/unf/transaction.cpp: -------------------------------------------------------------------------------- 1 | #include "unf/transaction.h" 2 | #include "unf/broker.h" 3 | #include "unf/capturePredicate.h" 4 | 5 | #include 6 | #include 7 | 8 | PXR_NAMESPACE_USING_DIRECTIVE 9 | 10 | namespace unf { 11 | 12 | NoticeTransaction::NoticeTransaction( 13 | const BrokerPtr& broker, CapturePredicate predicate) 14 | : _broker(broker) 15 | { 16 | _broker->BeginTransaction(predicate); 17 | } 18 | 19 | NoticeTransaction::NoticeTransaction( 20 | const BrokerPtr& broker, const CapturePredicateFunc& predicate) 21 | : _broker(broker) 22 | { 23 | _broker->BeginTransaction(predicate); 24 | } 25 | 26 | NoticeTransaction::NoticeTransaction( 27 | const UsdStageRefPtr& stage, CapturePredicate predicate) 28 | : _broker(Broker::Create(stage)) 29 | { 30 | _broker->BeginTransaction(predicate); 31 | } 32 | 33 | NoticeTransaction::NoticeTransaction( 34 | const PXR_NS::UsdStageRefPtr& stage, const CapturePredicateFunc& predicate) 35 | : _broker(Broker::Create(stage)) 36 | { 37 | _broker->BeginTransaction(predicate); 38 | } 39 | 40 | NoticeTransaction::~NoticeTransaction() { _broker->EndTransaction(); } 41 | 42 | } // namespace unf 43 | -------------------------------------------------------------------------------- /src/unf/dispatcher.cpp: -------------------------------------------------------------------------------- 1 | #include "unf/dispatcher.h" 2 | #include "unf/broker.h" 3 | #include "unf/notice.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | PXR_NAMESPACE_USING_DIRECTIVE 11 | 12 | namespace unf { 13 | 14 | TF_REGISTRY_FUNCTION(TfType) { TfType::Define(); } 15 | 16 | Dispatcher::Dispatcher(const BrokerWeakPtr& broker) : _broker(broker) {} 17 | 18 | void Dispatcher::Revoke() 19 | { 20 | for (auto& key : _keys) { 21 | TfNotice::Revoke(key); 22 | } 23 | } 24 | 25 | StageDispatcher::StageDispatcher(const BrokerWeakPtr& broker) 26 | : Dispatcher(broker) 27 | { 28 | } 29 | 30 | void StageDispatcher::Register() 31 | { 32 | _keys.reserve(4); 33 | 34 | _Register< 35 | UsdNotice::StageContentsChanged, 36 | UnfNotice::StageContentsChanged>(); 37 | _Register(); 38 | _Register< 39 | UsdNotice::StageEditTargetChanged, 40 | UnfNotice::StageEditTargetChanged>(); 41 | _Register(); 42 | } 43 | 44 | } // namespace unf 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # Environment 35 | .DS_Store 36 | .AppleDouble 37 | 38 | # Development 39 | build/ 40 | .vscode 41 | .project 42 | .settings 43 | .idea/ 44 | .history/ 45 | 46 | # CMake 47 | cmake-build-*/ 48 | 49 | # Python byte-compiled / optimized / DLL files 50 | __pycache__/ 51 | *.py[cod] 52 | *$py.class 53 | 54 | # Python Distribution / packaging 55 | .Python 56 | build/ 57 | develop-eggs/ 58 | dist/ 59 | downloads/ 60 | eggs/ 61 | .eggs/ 62 | lib/ 63 | lib64/ 64 | parts/ 65 | sdist/ 66 | var/ 67 | wheels/ 68 | share/python-wheels/ 69 | *.egg-info/ 70 | .installed.cfg 71 | *.egg 72 | MANIFEST 73 | _skbuild/ 74 | wheelhouse/ 75 | 76 | # Unit test / coverage reports 77 | .coverage 78 | .coverage.* 79 | .cache 80 | .pytest_cache/ 81 | .benchmarks/ 82 | 83 | # Environments 84 | .env 85 | .venv 86 | env/ 87 | venv/ 88 | ENV/ 89 | env.bak/ 90 | venv.bak/ 91 | -------------------------------------------------------------------------------- /src/python/predicate.h: -------------------------------------------------------------------------------- 1 | #ifndef USD_NOTICE_FRAMEWORK_PYTHON_PREDICATE_H 2 | #define USD_NOTICE_FRAMEWORK_PYTHON_PREDICATE_H 3 | 4 | #include "unf/broker.h" 5 | #include "unf/capturePredicate.h" 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #ifndef PXR_USE_INTERNAL_BOOST_PYTHON 12 | #include 13 | using namespace boost::python; 14 | #else 15 | #include 16 | using namespace PXR_BOOST_PYTHON_NAMESPACE; 17 | #endif 18 | 19 | #include 20 | 21 | using namespace unf; 22 | 23 | PXR_NAMESPACE_USING_DIRECTIVE 24 | 25 | using _CapturePredicateFuncRaw = bool(object const&); 26 | using _CapturePredicateFunc = std::function<_CapturePredicateFuncRaw>; 27 | 28 | static CapturePredicateFunc WrapPredicate(_CapturePredicateFunc fn) 29 | { 30 | // Capture by-copy to prevent boost object from being destroyed. 31 | return [=](const UnfNotice::StageNotice& notice) { 32 | TfPyLock lock; 33 | 34 | if (!fn) return true; 35 | 36 | // Creates a Python version of the notice by inspecting its type and 37 | // converting the generic StageNotice to the real notice inside. 38 | object _notice = Tf_PyNoticeObjectGenerator::Invoke(notice); 39 | return fn(_notice); 40 | }; 41 | } 42 | 43 | #endif // USD_NOTICE_FRAMEWORK_PYTHON_PREDICATE_H 44 | -------------------------------------------------------------------------------- /doc/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/doc") 2 | 3 | set(DOXYGEN_PROJECT_NAME "Usd Notice Framework") 4 | set(DOXYGEN_HTML_OUTPUT "doc/doxygen") 5 | set(DOXYGEN_EXTENSION_MAPPING "h=C++") 6 | set(DOXYGEN_GENERATE_HTML YES) 7 | set(DOXYGEN_GENERATE_LATEX NO) 8 | set(DOXYGEN_GENERATE_TREEVIEW YES) 9 | set(DOXYGEN_QUIET YES) 10 | set(DOXYGEN_SORT_MEMBER_DOCS NO) 11 | set(DOXYGEN_FULL_PATH_NAMES NO) 12 | set(DOXYGEN_FILE_PATTERNS *.dox *.h) 13 | 14 | set(DOXYGEN_GENERATE_TAGFILE "${CMAKE_CURRENT_BINARY_DIR}/doc/UNF.tag") 15 | set(DOXYGEN_TAGFILES 16 | "${PROJECT_SOURCE_DIR}/doc/doxygen/USD.tag \ 17 | = https://graphics.pixar.com/usd/release/api/") 18 | 19 | doxygen_add_docs(unfApiRefDoc 20 | "${PROJECT_SOURCE_DIR}/doc/doxygen/index.dox" 21 | "${PROJECT_SOURCE_DIR}/doc/doxygen/namespaces.dox" 22 | "${PROJECT_SOURCE_DIR}/src/unf" 23 | ) 24 | 25 | file(COPY sphinx DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) 26 | configure_file(sphinx/conf.py sphinx/conf.py @ONLY) 27 | 28 | sphinx_add_docs(unfDoc 29 | SOURCE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/sphinx" 30 | OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/doc" 31 | DEPENDS unfApiRefDoc 32 | ) 33 | 34 | add_custom_target(documentation ALL) 35 | add_dependencies(documentation 36 | unfDoc 37 | unfApiRefDoc 38 | ) 39 | 40 | install( 41 | DIRECTORY 42 | "${CMAKE_CURRENT_BINARY_DIR}/doc/" 43 | DESTINATION "${CMAKE_INSTALL_DOCDIR}" 44 | ) 45 | -------------------------------------------------------------------------------- /doc/sphinx/_static/style.css: -------------------------------------------------------------------------------- 1 | .changelog-release { 2 | position: relative; 3 | } 4 | 5 | .changelog-release > h1, h2, h3, h4, h5 { 6 | margin-bottom: 5px; 7 | } 8 | 9 | .changelog-release em.release-date { 10 | color: #999; 11 | position: absolute; 12 | line-height: 2.5em; 13 | top: 0; 14 | right: 0; 15 | } 16 | 17 | .changelog-release ul.changelog-change-list { 18 | list-style: outside none none; 19 | } 20 | 21 | .changelog-release ul.changelog-change-list > li { 22 | list-style: outside none none; 23 | position: relative; 24 | margin: 0; 25 | padding: 8px 0 2px 120px; 26 | border-top: 1px solid #D6D6D6; 27 | } 28 | 29 | .rst-content .section .changelog-change-list ul li { 30 | list-style: initial; 31 | } 32 | 33 | .changelog-category { 34 | border-right: 3px solid #CCC; 35 | box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.05), 0px -1px 0px rgba(0, 0, 0, 0.05) inset; 36 | color: #333; 37 | display: inline-block; 38 | font-size: 0.7em; 39 | font-style: normal; 40 | font-weight: bold; 41 | line-height: 14px; 42 | padding: 4px 2px 4px 10px; 43 | text-shadow: 1px 1px 0px #FFF; 44 | text-transform: uppercase; 45 | width: 102px; 46 | position: absolute; 47 | top: 10px; 48 | left: 0px; 49 | background-color: #f8f8f8; 50 | } 51 | 52 | .changelog-category-fixed { 53 | border-color: #7C0; 54 | } 55 | 56 | .changelog-category-new { 57 | border-color: #11B0E9; 58 | } 59 | 60 | .changelog-category-changed { 61 | border-color: #EB3F3F; 62 | } 63 | -------------------------------------------------------------------------------- /test/unit/python/test_broker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pxr import Usd 4 | import unf 5 | 6 | 7 | def test_broker_create(): 8 | """Create a broker from stage.""" 9 | stage = Usd.Stage.CreateInMemory() 10 | broker = unf.Broker.Create(stage) 11 | assert broker.GetStage() == stage 12 | 13 | def test_broker_create_twice(): 14 | """Create two brokers from stage.""" 15 | stage = Usd.Stage.CreateInMemory() 16 | broker1 = unf.Broker.Create(stage) 17 | broker2 = unf.Broker.Create(stage) 18 | assert broker1 == broker2 19 | 20 | def test_broker_transaction(): 21 | """Start and end a transaction.""" 22 | stage = Usd.Stage.CreateInMemory() 23 | broker = unf.Broker.Create(stage) 24 | 25 | broker.BeginTransaction() 26 | assert broker.IsInTransaction() is True 27 | 28 | broker.EndTransaction() 29 | assert broker.IsInTransaction() is False 30 | 31 | def test_broker_transaction_nested(): 32 | """Start and end a nested transaction.""" 33 | stage = Usd.Stage.CreateInMemory() 34 | broker = unf.Broker.Create(stage) 35 | 36 | broker.BeginTransaction() 37 | assert broker.IsInTransaction() is True 38 | 39 | broker.BeginTransaction() 40 | assert broker.IsInTransaction() is True 41 | 42 | broker.BeginTransaction() 43 | assert broker.IsInTransaction() is True 44 | 45 | broker.EndTransaction() 46 | assert broker.IsInTransaction() is True 47 | 48 | broker.EndTransaction() 49 | assert broker.IsInTransaction() is True 50 | 51 | broker.EndTransaction() 52 | assert broker.IsInTransaction() is False 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/docs-deploy.yml: -------------------------------------------------------------------------------- 1 | name: docs-deploy 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | concurrency: 15 | group: "pages" 16 | cancel-in-progress: false 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: 3.12 28 | 29 | - name: Create Build Environment 30 | run: | 31 | sudo apt update 32 | sudo apt install -y doxygen 33 | python -m pip install --upgrade pip 34 | python -m pip install -r ${{github.workspace}}/doc/requirements.txt 35 | mkdir -p ${{github.workspace}}/build 36 | 37 | - name: Build documentation 38 | run: | 39 | export BUILD_DOCS_WITHOUT_CMAKE=1 40 | export PYTHONPATH="${{github.workspace}}/doc/sphinx" 41 | sphinx-build -T -E -b html \ 42 | "${{github.workspace}}/doc/sphinx" \ 43 | "${{github.workspace}}/build" 44 | 45 | - name: Upload artifact 46 | uses: actions/upload-pages-artifact@v3 47 | with: 48 | path: ${{github.workspace}}/build 49 | 50 | deploy: 51 | environment: 52 | name: github-pages 53 | url: ${{ steps.deployment.outputs.page_url }} 54 | 55 | runs-on: ubuntu-latest 56 | needs: build 57 | 58 | steps: 59 | - name: Deploy to GitHub Pages 60 | id: deployment 61 | uses: actions/deploy-pages@v4 62 | -------------------------------------------------------------------------------- /doc/sphinx/introduction.rst: -------------------------------------------------------------------------------- 1 | .. _introduction: 2 | 3 | ************ 4 | Introduction 5 | ************ 6 | 7 | The USD Notice Framework (UNF) is built over the native 8 | :term:`Tf Notification System` in :term:`USD`, an open source extensible 9 | software platform for collaboratively constructing animated 3D scenes. It 10 | provides a :ref:`C++ ` and 11 | :ref:`Python ` API to efficiently manage the flow of 12 | notifications emitted when authoring the :term:`USD` stage. 13 | 14 | While :term:`USD` notices are delivered synchronously and tightly coupled with 15 | the sender, UNF introduces :ref:`standalone notices ` that can be used 16 | for deferred delivery and can be aggregated per notice type, when applicable. 17 | 18 | What does this solve? 19 | ===================== 20 | 21 | Pixar designed :term:`USD` as an open and extensible framework for composable 22 | data interchange across different tools. As such, it is highly optimized for 23 | that purpose. Born out of Pixar's :term:`Presto Animation` package, some 24 | application-level features were intentionally omitted to maintain speed, 25 | scalability, and robustness to support its core usage. 26 | 27 | When editing :term:`USD` data, the stage and layers produce a high volume of 28 | change notifications that can be hard to manage when crafting a performant user 29 | experience. UNF provides a framework to aggregate and even simplify change 30 | notifications across a series of edits on a :term:`USD` stage. It allows 31 | developers to build performant and sustainable interactive applications using 32 | :term:`USD` as its native data model. 33 | 34 | .. seealso:: :ref:`getting_started` 35 | -------------------------------------------------------------------------------- /test/utility/unfTest/notice.cpp: -------------------------------------------------------------------------------- 1 | #include "notice.h" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | PXR_NAMESPACE_USING_DIRECTIVE 11 | 12 | namespace Test { 13 | 14 | TF_REGISTRY_FUNCTION(TfType) 15 | { 16 | TfType:: 17 | Define >(); 18 | 19 | TfType::Define< 20 | UnMergeableNotice, 21 | TfType::Bases >(); 22 | 23 | TfType::Define >(); 24 | 25 | TfType:: 26 | Define >(); 27 | 28 | TfType:: 29 | Define >(); 30 | } 31 | 32 | MergeableNotice::MergeableNotice(const DataMap& data) : _data(data) {} 33 | 34 | MergeableNotice::MergeableNotice(const MergeableNotice& other) 35 | : _data(other._data) 36 | { 37 | } 38 | 39 | MergeableNotice& MergeableNotice::operator=(const MergeableNotice& other) 40 | { 41 | MergeableNotice copy(other); 42 | std::swap(_data, copy._data); 43 | return *this; 44 | } 45 | 46 | void MergeableNotice::Merge(MergeableNotice&& notice) 47 | { 48 | for (const auto& it : notice.GetData()) { 49 | _data[it.first] = std::move(it.second); 50 | } 51 | } 52 | 53 | const DataMap& MergeableNotice::GetData() const { return _data; } 54 | 55 | bool UnMergeableNotice::IsMergeable() const { return false; } 56 | 57 | InputNotice::InputNotice() {} 58 | 59 | OutputNotice1::OutputNotice1(const InputNotice&) {} 60 | 61 | OutputNotice2::OutputNotice2(const InputNotice&) {} 62 | 63 | } // namespace Test 64 | -------------------------------------------------------------------------------- /test/unit/testDispatcherPlugin.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | class DispatcherTest : public ::testing::Test { 15 | protected: 16 | using StageDispatcherPtr = PXR_NS::TfRefPtr; 17 | using NewStageDispatcherPtr = PXR_NS::TfRefPtr<::Test::NewStageDispatcher>; 18 | using TestDispatcherPtr = PXR_NS::TfRefPtr<::Test::NewDispatcher>; 19 | 20 | using Listener = ::Test::Listener< 21 | ::Test::InputNotice, ::Test::OutputNotice1, ::Test::OutputNotice2>; 22 | 23 | void SetUp() override 24 | { 25 | _stage = PXR_NS::UsdStage::CreateInMemory(); 26 | _listener.SetStage(_stage); 27 | } 28 | 29 | PXR_NS::UsdStageRefPtr _stage; 30 | Listener _listener; 31 | }; 32 | 33 | TEST_F(DispatcherTest, Discover) 34 | { 35 | auto broker = unf::Broker::Create(_stage); 36 | 37 | // Ensure that stage dispacther has been replaced as expected. 38 | auto dispatcher = broker->GetDispatcher("StageDispatcher"); 39 | ASSERT_TRUE(PXR_NS::TfDynamic_cast(dispatcher)); 40 | ASSERT_FALSE(PXR_NS::TfDynamic_cast(dispatcher)); 41 | 42 | // Sending InputNotice now triggers OutputNotice1 and OutputNotice2. 43 | ::Test::InputNotice().Send(PXR_NS::TfWeakPtr(_stage)); 44 | 45 | ASSERT_EQ(_listener.Received<::Test::OutputNotice1>(), 1); 46 | ASSERT_EQ(_listener.Received<::Test::OutputNotice2>(), 1); 47 | } 48 | -------------------------------------------------------------------------------- /doc/sphinx/conf.py: -------------------------------------------------------------------------------- 1 | """Configuration file for the Sphinx documentation builder.""" 2 | 3 | import re 4 | import pathlib 5 | import os 6 | import sys 7 | 8 | root = pathlib.Path(__file__).parent.resolve() 9 | sys.path.insert(0, str(root / "_extensions")) 10 | 11 | extensions = ["changelog", "sphinxcontrib.doxylink"] 12 | 13 | # This deployment mode exists so that documentation can be built within a 14 | # Github runner without requiring USD/TBB to be installed. We would be 15 | # able to simplify the process if we could rely on pre-built dependencies which 16 | # will be quick to install. 17 | if os.environ.get("BUILD_DOCS_WITHOUT_CMAKE"): 18 | import doxygen 19 | 20 | doxygen.create_cmake_config() 21 | build_path = doxygen.build() 22 | source_path = root.parent.parent.resolve() 23 | 24 | path = source_path / "CMakeLists.txt" 25 | data = path.read_text() 26 | 27 | pattern = r"project\(.* VERSION ([\d\\.]+)" 28 | version = re.search(pattern, data, re.DOTALL).group(1) 29 | 30 | html_extra_path = ["./api"] 31 | 32 | else: 33 | build_path = pathlib.Path("@CMAKE_CURRENT_BINARY_DIR@") / "doc" 34 | source_path = pathlib.Path("@PROJECT_SOURCE_DIR@") 35 | 36 | version = "@CMAKE_PROJECT_VERSION@" 37 | 38 | source_suffix = ".rst" 39 | master_doc = "index" 40 | 41 | project = u"USD Notice Framework" 42 | copyright = u"2023, Walt Disney Animation Studio" 43 | release = version 44 | 45 | doxylink = { 46 | "usd-cpp": ( 47 | str(source_path / "doc" / "doxygen" / "USD.tag"), 48 | "https://graphics.pixar.com/usd/release/api" 49 | ), 50 | "unf-cpp": ( 51 | str(build_path / "UNF.tag"), 52 | "./doxygen" 53 | ) 54 | } 55 | 56 | html_theme = "sphinx_rtd_theme" 57 | html_favicon = "favicon.png" 58 | html_static_path = ["_static"] 59 | html_css_files = ["style.css"] 60 | 61 | -------------------------------------------------------------------------------- /cmake/modules/FindClangFormat.cmake: -------------------------------------------------------------------------------- 1 | # Discover required Clang-format target. 2 | # 3 | # This module defines the following imported targets: 4 | # Clangformat 5 | # 6 | # It also create the target 'format' to apply clang-format rules to project. 7 | # 8 | # Usage: 9 | # find_package(ClangFormat) 10 | # find_package(ClangFormat REQUIRED) 11 | # find_package(ClangFormat 7.0.1 REQUIRED) 12 | # 13 | # Note: 14 | # The ClangFormat_ROOT environment variable or CMake variable can be used to 15 | # prepend a custom search path. 16 | # (https://cmake.org/cmake/help/latest/policy/CMP0074.html) 17 | 18 | include(FindPackageHandleStandardArgs) 19 | 20 | find_program(CLANG_FORMAT NAMES clang-format) 21 | 22 | if(CLANG_FORMAT) 23 | execute_process( 24 | COMMAND "${CLANG_FORMAT}" --version 25 | OUTPUT_VARIABLE _version 26 | ERROR_VARIABLE _version 27 | OUTPUT_STRIP_TRAILING_WHITESPACE 28 | ) 29 | 30 | if (_version MATCHES "version ([0-9]+)") 31 | set(CLANG_FORMAT_VERSION "${CMAKE_MATCH_1}") 32 | endif() 33 | 34 | mark_as_advanced(_version) 35 | endif() 36 | 37 | mark_as_advanced(CLANG_FORMAT CLANG_FORMAT_VERSION) 38 | 39 | find_package_handle_standard_args( 40 | ClangFormat 41 | REQUIRED_VARS 42 | CLANG_FORMAT 43 | VERSION_VAR 44 | CLANG_FORMAT_VERSION 45 | ) 46 | 47 | if (ClangFormat_FOUND AND NOT TARGET ClangFormat) 48 | add_executable(ClangFormat IMPORTED GLOBAL) 49 | set_target_properties(ClangFormat PROPERTIES 50 | IMPORTED_LOCATION "${CLANG_FORMAT}" 51 | ) 52 | 53 | file(GLOB_RECURSE ALL_SOURCE_FILES 54 | ${PROJECT_SOURCE_DIR}/*.cpp 55 | ${PROJECT_SOURCE_DIR}/*.h) 56 | 57 | add_custom_target( 58 | format 59 | COMMAND ClangFormat -i ${ALL_SOURCE_FILES} 60 | COMMENT "Apply clang-format to project" 61 | ) 62 | endif() 63 | -------------------------------------------------------------------------------- /test/utility/unfTest/observer.h: -------------------------------------------------------------------------------- 1 | #ifndef TEST_USD_NOTICE_FRAMEWORK_OBSERVER_H 2 | #define TEST_USD_NOTICE_FRAMEWORK_OBSERVER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | namespace Test { 13 | 14 | // Interface to examine content of notice received. 15 | 16 | template 17 | class Observer : public PXR_NS::TfWeakBase { 18 | public: 19 | Observer() = default; 20 | Observer(const PXR_NS::UsdStageWeakPtr& stage) : _count(0) 21 | { 22 | SetStage(stage); 23 | } 24 | 25 | virtual ~Observer() { PXR_NS::TfNotice::Revoke(_key); } 26 | 27 | void SetStage(const PXR_NS::UsdStageWeakPtr& stage) 28 | { 29 | auto self = PXR_NS::TfCreateWeakPtr(this); 30 | _key = PXR_NS::TfNotice::Register( 31 | PXR_NS::TfCreateWeakPtr(this), &Observer::OnReceiving, stage); 32 | } 33 | 34 | void SetCallback(std::function callback) 35 | { 36 | _callback = callback; 37 | } 38 | 39 | const T& GetLatestNotice() const 40 | { 41 | if (!_notice) { 42 | throw std::runtime_error("Impossible to access latest notice."); 43 | } 44 | return *_notice; 45 | } 46 | 47 | size_t Received() const { return _count; } 48 | 49 | void Reset() 50 | { 51 | _count = 0; 52 | _notice.reset(); 53 | } 54 | 55 | private: 56 | void OnReceiving(const T& notice, const PXR_NS::UsdStageWeakPtr&) 57 | { 58 | _notice = notice; 59 | _count++; 60 | 61 | if (_callback) { 62 | _callback(notice); 63 | } 64 | } 65 | 66 | std::optional _notice; 67 | size_t _count; 68 | PXR_NS::TfNotice::Key _key; 69 | std::function _callback; 70 | }; 71 | 72 | } // namespace Test 73 | 74 | #endif // TEST_USD_NOTICE_FRAMEWORK_OBSERVER_H 75 | -------------------------------------------------------------------------------- /src/unf/capturePredicate.h: -------------------------------------------------------------------------------- 1 | #ifndef USD_NOTICE_FRAMEWORK_CAPTURE_PREDICATE_H 2 | #define USD_NOTICE_FRAMEWORK_CAPTURE_PREDICATE_H 3 | 4 | /// \file unf/capturePredicate.h 5 | 6 | #include "unf/api.h" 7 | #include "unf/notice.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | namespace unf { 14 | 15 | /// Convenient alias for function defining whether notice can be captured. 16 | using CapturePredicateFunc = std::function; 17 | 18 | /// \class CapturePredicate 19 | /// 20 | /// \brief 21 | /// Predicate functor which indicates whether a notice can be captured during 22 | /// a transaction. 23 | /// 24 | /// Common predicates are provided as static methods for convenience. 25 | /// 26 | /// \note 27 | /// We used a functor embedding a CapturePredicateFunc instead of defining 28 | /// common predicates via free functions to simplify the Python binding process. 29 | class CapturePredicate { 30 | public: 31 | /// \brief 32 | /// Create predicate from a \p function. 33 | /// 34 | /// The following example will create a predicate which will return false 35 | /// for a 'Foo' notice. 36 | /// 37 | /// \code{.cpp} 38 | /// CapturePredicate([&](const unf::UnfNotice::StageNotice& n) { 39 | /// return (n.GetTypeId() != typeid(Foo).name()); 40 | /// }); 41 | /// \endcode 42 | UNF_API CapturePredicate(const CapturePredicateFunc&); 43 | 44 | /// Invoke boolean predicate on UnfNotice::StageNotice \p notice. 45 | UNF_API bool operator()(const UnfNotice::StageNotice&) const; 46 | 47 | /// Create a predicate which return true for each notice type. 48 | UNF_API static CapturePredicate Default(); 49 | 50 | /// Create a predicate which return false for each notice type. 51 | UNF_API static CapturePredicate BlockAll(); 52 | 53 | private: 54 | CapturePredicateFunc _function = nullptr; 55 | }; 56 | 57 | } // namespace unf 58 | 59 | #endif // USD_NOTICE_FRAMEWORK_CAPTURE_PREDICATE_H 60 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(unf 2 | unf/broker.cpp 3 | unf/capturePredicate.cpp 4 | unf/dispatcher.cpp 5 | unf/notice.cpp 6 | unf/transaction.cpp 7 | ) 8 | 9 | # Suppress warning sfrom USD regarding deprecated TBB features. 10 | # https://github.com/PixarAnimationStudios/OpenUSD/issues/1471 11 | target_compile_definitions(unf 12 | PUBLIC TBB_SUPPRESS_DEPRECATED_MESSAGES 13 | ) 14 | 15 | # Suppress warnings from USD library including . 16 | # https://github.com/PixarAnimationStudios/OpenUSD/issues/1057 17 | target_compile_definitions(unf 18 | PUBLIC _GLIBCXX_PERMIT_BACKWARD_HASH 19 | ) 20 | 21 | # Ensure that all symbols are exported 22 | target_compile_definitions(unf 23 | PRIVATE 24 | UNF_EXPORTS=1 25 | ) 26 | 27 | target_include_directories(unf 28 | PUBLIC 29 | $ 30 | $ 31 | ) 32 | 33 | target_link_libraries(unf 34 | PUBLIC 35 | usd::arch 36 | usd::plug 37 | usd::sdf 38 | usd::tf 39 | usd::usd 40 | usd::vt 41 | ) 42 | 43 | # Transitive Pixar libraries depend on vendorized Boost.Python 44 | # (Required due to manual CMake module used to locate USD) 45 | if (BUILD_PYTHON_BINDINGS AND USD_USE_INTERNAL_BOOST_PYTHON) 46 | target_link_libraries(unf PUBLIC usd::boost usd::python) 47 | endif() 48 | 49 | install( 50 | TARGETS unf 51 | EXPORT ${PROJECT_NAME} 52 | ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} 53 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} 54 | RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR} 55 | ) 56 | 57 | install( 58 | DIRECTORY 59 | unf 60 | DESTINATION 61 | ${CMAKE_INSTALL_INCLUDEDIR} 62 | FILES_MATCHING PATTERN "*.h" 63 | ) 64 | 65 | install(EXPORT ${PROJECT_NAME} 66 | FILE unf-targets.cmake 67 | NAMESPACE unf:: 68 | DESTINATION share/cmake/unf 69 | ) 70 | 71 | if (BUILD_PYTHON_BINDINGS) 72 | add_subdirectory(python) 73 | endif() 74 | -------------------------------------------------------------------------------- /test/unit/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(testUnitBroker testBroker.cpp) 2 | target_link_libraries(testUnitBroker 3 | PRIVATE 4 | unf 5 | GTest::gtest 6 | GTest::gtest_main 7 | ) 8 | gtest_discover_tests(testUnitBroker) 9 | 10 | add_executable(testUnitBrokerFlow testBrokerFlow.cpp) 11 | target_link_libraries(testUnitBrokerFlow 12 | PRIVATE 13 | unf 14 | unfTest 15 | GTest::gtest 16 | GTest::gtest_main 17 | ) 18 | gtest_discover_tests(testUnitBrokerFlow) 19 | 20 | add_executable(testUnitDispatcher testDispatcher.cpp) 21 | target_link_libraries(testUnitDispatcher 22 | PRIVATE 23 | unf 24 | unfTest 25 | unfTestNewStageDispatcher 26 | unfTestNewDispatcher 27 | GTest::gtest 28 | GTest::gtest_main 29 | ) 30 | gtest_discover_tests(testUnitDispatcher) 31 | 32 | add_executable(testUnitDispatcherPlugin testDispatcherPlugin.cpp) 33 | target_link_libraries(testUnitDispatcherPlugin 34 | PRIVATE 35 | unf 36 | unfTest 37 | unfTestNewStageDispatcher 38 | unfTestNewDispatcher 39 | GTest::gtest 40 | GTest::gtest_main 41 | ) 42 | set(_path "${CMAKE_BINARY_DIR}/test/utility/plugins/*Dispatcher/plugInfo_$.json") 43 | gtest_discover_tests( 44 | testUnitDispatcherPlugin 45 | PROPERTIES ENVIRONMENT 46 | "PXR_PLUGINPATH_NAME=${_path}$,;,:>$ENV{PXR_PLUGINPATH_NAME}" 47 | 48 | ) 49 | add_executable(testUnitTransaction testTransaction.cpp) 50 | target_link_libraries(testUnitTransaction 51 | PRIVATE 52 | unf 53 | unfTest 54 | GTest::gtest 55 | GTest::gtest_main 56 | ) 57 | gtest_discover_tests(testUnitTransaction) 58 | 59 | add_executable(testUnitObjectsChanged testObjectsChanged.cpp) 60 | target_link_libraries(testUnitObjectsChanged 61 | PRIVATE 62 | unf 63 | unfTest 64 | GTest::gtest 65 | GTest::gtest_main 66 | ) 67 | gtest_discover_tests(testUnitObjectsChanged) 68 | 69 | if (BUILD_PYTHON_BINDINGS) 70 | add_subdirectory(python) 71 | endif() 72 | -------------------------------------------------------------------------------- /test/utility/unfTest/notice.h: -------------------------------------------------------------------------------- 1 | #ifndef TEST_USD_NOTICE_FRAMEWORK_NOTICE_H 2 | #define TEST_USD_NOTICE_FRAMEWORK_NOTICE_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | namespace Test { 13 | 14 | using DataMap = std::unordered_map; 15 | 16 | // Notice with can be consolidated within broker transactions. 17 | class MergeableNotice 18 | : public unf::UnfNotice::StageNoticeImpl { 19 | public: 20 | UNF_API MergeableNotice() = default; 21 | UNF_API MergeableNotice(const DataMap& data); 22 | 23 | UNF_API MergeableNotice(const MergeableNotice& other); 24 | 25 | UNF_API MergeableNotice& operator=(const MergeableNotice& other); 26 | 27 | UNF_API virtual ~MergeableNotice() = default; 28 | 29 | // Bring all Merge declarations from base class to prevent 30 | // overloaded-virtual warning. 31 | using unf::UnfNotice::StageNoticeImpl::Merge; 32 | 33 | UNF_API virtual void Merge(MergeableNotice&& notice) override; 34 | 35 | UNF_API const DataMap& GetData() const; 36 | 37 | private: 38 | DataMap _data; 39 | }; 40 | 41 | // Notice which cannot be consolidated within broker transactions. 42 | class UnMergeableNotice 43 | : public unf::UnfNotice::StageNoticeImpl { 44 | public: 45 | UNF_API UnMergeableNotice() = default; 46 | UNF_API virtual ~UnMergeableNotice() = default; 47 | 48 | UNF_API virtual bool IsMergeable() const; 49 | }; 50 | 51 | // Declare notices used by the test dispatchers. 52 | class InputNotice : public PXR_NS::TfNotice { 53 | public: 54 | UNF_API InputNotice(); 55 | }; 56 | 57 | class OutputNotice1 : public unf::UnfNotice::StageNoticeImpl { 58 | public: 59 | UNF_API OutputNotice1(const InputNotice&); 60 | }; 61 | 62 | class OutputNotice2 : public unf::UnfNotice::StageNoticeImpl { 63 | public: 64 | UNF_API OutputNotice2(const InputNotice&); 65 | }; 66 | 67 | } // namespace Test 68 | 69 | #endif // TEST_USD_NOTICE_FRAMEWORK_NOTICE_H 70 | -------------------------------------------------------------------------------- /doc/sphinx/api_reference/python/Notice/ObjectsChanged.rst: -------------------------------------------------------------------------------- 1 | ************************* 2 | unf.Notice.ObjectsChanged 3 | ************************* 4 | 5 | .. py:class:: unf.Notice.ObjectsChanged 6 | 7 | Base: :py:class:`unf.Notice.StageNotice` 8 | 9 | Notice sent in response to authored changes that affect any 10 | Usd Object. 11 | 12 | This notice type is the standalone equivalent of the 13 | :usd-cpp:`UsdNotice::ObjectsChanged` notice type. 14 | 15 | .. py:method:: AffectedObject(object) 16 | 17 | Indicate whether object was affected by the change that generated 18 | this notice. 19 | 20 | :param object: Instance of Usd Object. 21 | 22 | :return: Boolean value. 23 | 24 | .. py:method:: ResyncedObject(object) 25 | 26 | Indicate whether object was resynced by the change that generated 27 | this notice. 28 | 29 | :param object: Instance of Usd Object. 30 | 31 | :return: Boolean value. 32 | 33 | .. py:method:: ChangedInfoOnly(object) 34 | 35 | Indicate whether object was modified but not resynced by the 36 | change that generated this notice. 37 | 38 | :param object: Instance of Usd Object. 39 | 40 | :return: Boolean value. 41 | 42 | .. py:method:: GetResyncedPaths() 43 | 44 | Return list of paths that are resynced in lexicographical order. 45 | 46 | :return: List of instances of Sdf Path. 47 | 48 | .. py:method:: GetChangedInfoOnlyPaths() 49 | 50 | Return list of paths that are modified but not resynced in 51 | lexicographical order. 52 | 53 | :return: List of instances of Sdf Path. 54 | 55 | .. py:method:: GetChangedFields(target) 56 | 57 | Return the set of changed fields in layers that affected the *target*. 58 | 59 | :param target: Instance of Usd Object or Sdf Path. 60 | 61 | :return: List of field name. 62 | 63 | .. py:method:: HasChangedFields(target) 64 | 65 | Indicate whether any changed fields affected the *target*. 66 | 67 | :param target: Instance of Usd Object or Sdf Path. 68 | 69 | :return: Boolean value. 70 | -------------------------------------------------------------------------------- /src/python/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(pyUnf SHARED 2 | module.cpp 3 | wrapBroker.cpp 4 | wrapCapturePredicate.cpp 5 | wrapNotice.cpp 6 | wrapTransaction.cpp 7 | ) 8 | 9 | # Supress warnings from Boost Python regarding deprecated Bind placeholders. 10 | # https://github.com/boostorg/python/issues/359 11 | target_compile_definitions( 12 | pyUnf PRIVATE BOOST_BIND_GLOBAL_PLACEHOLDERS 13 | ) 14 | 15 | target_compile_definitions( 16 | pyUnf PRIVATE BOOST_PYTHON_NO_PY_SIGNATURES 17 | ) 18 | 19 | set_target_properties(pyUnf 20 | PROPERTIES 21 | RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/src/python/$/unf" 22 | LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/src/python/$/unf" 23 | ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/src/python/$/unf" 24 | ) 25 | 26 | target_include_directories(pyUnf 27 | PUBLIC 28 | $ 29 | $ 30 | ) 31 | 32 | target_link_libraries(pyUnf PUBLIC unf) 33 | 34 | if (NOT USD_USE_INTERNAL_BOOST_PYTHON) 35 | target_link_libraries(pyUnf PUBLIC Boost::python) 36 | else() 37 | target_link_libraries(pyUnf PUBLIC usd::boost usd::python) 38 | endif() 39 | 40 | set_target_properties(pyUnf 41 | PROPERTIES 42 | PREFIX "" 43 | OUTPUT_NAME _unf 44 | LIBRARY_OUTPUT_DIRECTORY unf 45 | ) 46 | 47 | if(WIN32) 48 | # Python modules must be suffixed with .pyd on Windows. 49 | set_target_properties(pyUnf 50 | PROPERTIES 51 | SUFFIX ".pyd" 52 | ) 53 | endif() 54 | 55 | target_compile_definitions( 56 | pyUnf 57 | PRIVATE 58 | BOOST_PYTHON_NO_PY_SIGNATURES 59 | PXR_PYTHON_ENABLED=1 60 | PXR_PYTHON_MODULES_ENABLED=1 61 | MFB_PACKAGE_MODULE=unf 62 | MFB_PACKAGE_NAME=unf 63 | MFB_ALT_PACKAGE_NAME=unf 64 | ) 65 | 66 | file( 67 | GENERATE 68 | OUTPUT "$/__init__.py" 69 | INPUT "__init__.py" 70 | ) 71 | 72 | install( 73 | TARGETS pyUnf 74 | LIBRARY DESTINATION ${PYTHON_DESTINATION}/unf 75 | RUNTIME DESTINATION ${PYTHON_DESTINATION}/unf 76 | ) 77 | install( 78 | FILES __init__.py 79 | DESTINATION ${PYTHON_DESTINATION}/unf 80 | ) 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # USD Notice Framework 2 | 3 | [![CMake](https://img.shields.io/badge/CMake-3.20...3.30-blue.svg?logo=CMake&logoColor=blue)](https://cmake.org) 4 | [![test-linux](https://github.com/wdas/unf/actions/workflows/test-linux.yml/badge.svg?branch=main)](https://github.com/wdas/unf/actions/workflows/test-linux.yml) 5 | [![test-windows](https://github.com/wdas/unf/actions/workflows/test-windows.yml/badge.svg?branch=main)](https://github.com/wdas/unf/actions/workflows/test-windows.yml) 6 | [![License](https://img.shields.io/badge/License-TOST-yellow.svg)](https://github.com/wdas/unf/blob/main/LICENSE.txt) 7 | 8 | The USD Notice Framework (UNF) is built over the native [Tf Notification System][1] 9 | in [USD][2], an open source extensible software platform for collaboratively 10 | constructing animated 3D scenes. It provides a C++ and Python API to efficiently 11 | manage the flow of notifications emitted when authoring the USD stage. 12 | 13 | While USD notices are delivered synchronously and tightly coupled with 14 | the sender, UNF introduces standalone notices that can be used 15 | for deferred delivery and can be aggregated per notice type, when applicable. 16 | 17 | ## What does this solve? 18 | 19 | Pixar designed [USD][2] as an open and extensible framework for composable data 20 | interchange across different tools. As such, it is highly optimized for that 21 | purpose. Born out of Pixar's [Presto Animation][3] package, some 22 | application-level features were intentionally omitted to maintain speed, 23 | scalability, and robustness to support its core usage. 24 | 25 | When editing USD data, the stage and layers produce a high volume of change 26 | notifications that can be hard to manage when crafting a performant user 27 | experience. UNF provides a framework to aggregate and even simplify change 28 | notifications across a series of edits on a USD stage. It allows developers 29 | to build performant and sustainable interactive applications using USD as its 30 | native data model. 31 | 32 | ## Documentation 33 | 34 | Full documentation, including installation and setup guides, can be found at 35 | https://wdas.github.io/unf 36 | 37 | [1]: https://graphics.pixar.com/usd/release/api/page_tf__notification.html 38 | [2]: http://openusd.org 39 | [3]: https://en.wikipedia.org/wiki/Presto_(animation_software) 40 | -------------------------------------------------------------------------------- /doc/sphinx/api_reference/python/NoticeTransaction.rst: -------------------------------------------------------------------------------- 1 | ********************* 2 | unf.NoticeTransaction 3 | ********************* 4 | 5 | .. py:class:: unf.NoticeTransaction 6 | 7 | Context manager object which consolidates and filter notices derived from 8 | :class:`unf.Notice.StageNotice` within a specific scope. 9 | 10 | A transaction can be started with a :class:`~unf.Broker` instance, or with 11 | a Usd Stage instance. If the later option is chosen, a :class:`~unf.Broker` 12 | instance will be automatically created if none is associated with the 13 | incoming stage. 14 | 15 | .. code-block:: python 16 | 17 | # Create a transaction from a broker. 18 | with NoticeTransaction(broker) as transaction: 19 | ... 20 | 21 | # Create a transaction from a stage. 22 | with NoticeTransaction(stage) as transaction: 23 | broker = transaction.GetBroker() 24 | 25 | 26 | By default, all :class:`unf.Notice.StageNotice` notices will be 27 | captured during the entire scope of the transaction. A function 28 | *predicate* or a :class:`unf.CapturePredicate` instance can be passed to 29 | influence which notices are captured. 30 | 31 | Notices that are not captured will not be emitted. 32 | 33 | .. code-block:: python 34 | 35 | # Block all notices emitted within the transaction. 36 | with NoticeTransaction( 37 | broker, predicate=CapturePredicate.BlockAll() 38 | ) as transaction: 39 | ... 40 | 41 | # Block all notices from type 'Foo' emitted during transaction. 42 | with NoticeTransaction( 43 | broker, predicate=lambda n: not isinstance(n, Foo) 44 | ) as transaction: 45 | ... 46 | 47 | .. py:method:: __init__(target, predicate=CapturePredicate.Default()) 48 | 49 | :param target: Instance of :class:`unf.Broker` or Usd Stage. 50 | 51 | :param predicate: Instance of :class:`unf.CapturePredicate` or function 52 | taking a :class:`unf.Notice.StageNotice` instance and returning a 53 | boolean value. By default, the :meth:`unf.CapturePredicate.Default` 54 | predicate is used. 55 | 56 | :return: Instance of :class:`unf.NoticeTransaction`. 57 | 58 | .. py:method:: GetBroker() 59 | 60 | Return associated :class:`unf.Broker` instance. 61 | 62 | :return: Instance of :class:`unf.Broker`. 63 | -------------------------------------------------------------------------------- /test/utility/unfTest/listener.h: -------------------------------------------------------------------------------- 1 | #ifndef TEST_USD_NOTICE_FRAMEWORK_LISTENER_H 2 | #define TEST_USD_NOTICE_FRAMEWORK_LISTENER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | namespace Test { 14 | 15 | // Container to listen to several types of Tf notices. 16 | template 17 | class Listener : public PXR_NS::TfWeakBase { 18 | public: 19 | Listener() = default; 20 | Listener(const PXR_NS::UsdStageWeakPtr& stage) { SetStage(stage); } 21 | 22 | virtual ~Listener() 23 | { 24 | for (auto& element : _keys) { 25 | PXR_NS::TfNotice::Revoke(element.second); 26 | } 27 | } 28 | 29 | void SetStage(const PXR_NS::UsdStageWeakPtr& stage) 30 | { 31 | auto self = PXR_NS::TfCreateWeakPtr(this); 32 | _keys = std::unordered_map( 33 | {_Register(self, stage)...}); 34 | } 35 | 36 | template 37 | size_t Received() 38 | { 39 | std::string name = typeid(T).name(); 40 | if (_received.find(name) == _received.end()) return 0; 41 | 42 | return _received.at(name); 43 | } 44 | 45 | void Reset() 46 | { 47 | for (auto& element : _received) { 48 | element.second = 0; 49 | } 50 | } 51 | 52 | private: 53 | template 54 | std::pair _Register( 55 | const PXR_NS::TfWeakPtr& self, 56 | const PXR_NS::UsdStageWeakPtr& stage) 57 | { 58 | auto cb = &Listener::_Callback; 59 | std::string name = typeid(T).name(); 60 | auto key = PXR_NS::TfNotice::Register(self, cb, stage); 61 | 62 | return std::make_pair(name, key); 63 | } 64 | 65 | template 66 | void _Callback(const T& notice, const PXR_NS::UsdStageWeakPtr& sender) 67 | { 68 | std::string name = typeid(T).name(); 69 | 70 | if (_received.find(name) == _received.end()) _received[name] = 0; 71 | 72 | _received[name] += 1; 73 | } 74 | 75 | std::unordered_map _keys; 76 | std::unordered_map _received; 77 | }; 78 | 79 | } // namespace Test 80 | 81 | #endif // TEST_USD_NOTICE_FRAMEWORK_LISTENER_H 82 | -------------------------------------------------------------------------------- /src/python/wrapBroker.cpp: -------------------------------------------------------------------------------- 1 | // clang-format off 2 | 3 | #include "./predicate.h" 4 | 5 | #include "unf/broker.h" 6 | #include "unf/capturePredicate.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #ifndef PXR_USE_INTERNAL_BOOST_PYTHON 17 | #include 18 | using namespace boost::python; 19 | using noncopyable = boost::noncopyable; 20 | #else 21 | #include 22 | using namespace PXR_BOOST_PYTHON_NAMESPACE; 23 | using noncopyable = PXR_BOOST_PYTHON_NAMESPACE::noncopyable; 24 | #endif 25 | 26 | using namespace unf; 27 | 28 | PXR_NAMESPACE_USING_DIRECTIVE 29 | 30 | void Broker_BeginTransaction_WithFunc(Broker& self, object predicate) 31 | { 32 | auto _predicate = WrapPredicate(predicate); 33 | self.BeginTransaction(_predicate); 34 | } 35 | 36 | void wrapBroker() 37 | { 38 | // Ensure that predicate function can be passed from Python. 39 | TfPyFunctionFromPython<_CapturePredicateFuncRaw>(); 40 | 41 | class_( 42 | "Broker", 43 | "Intermediate object between the Usd Stage and any clients that needs " 44 | "asynchronous handling and upstream filtering of notices.", 45 | no_init) 46 | 47 | .def(TfPyRefAndWeakPtr()) 48 | 49 | .def( 50 | "Create", 51 | &Broker::Create, 52 | arg("stage"), 53 | "Create a broker from a Usd Stage.", 54 | return_value_policy >()) 55 | .staticmethod("Create") 56 | 57 | .def( 58 | "GetStage", 59 | &Broker::GetStage, 60 | "Return Usd Stage associated with the broker.", 61 | return_value_policy()) 62 | 63 | .def( 64 | "IsInTransaction", 65 | &Broker::IsInTransaction, 66 | "Indicate whether a notice transaction has been started.") 67 | 68 | .def( 69 | "BeginTransaction", 70 | (void(Broker::*)(CapturePredicate)) & Broker::BeginTransaction, 71 | (arg("predicate") = CapturePredicate::Default()), 72 | "Start a notice transaction.") 73 | 74 | .def( 75 | "BeginTransaction", 76 | &Broker_BeginTransaction_WithFunc, 77 | ((arg("self"), arg("predicate"))), 78 | "Start a notice transaction with a function predicate.") 79 | 80 | .def( 81 | "EndTransaction", 82 | &Broker::EndTransaction, 83 | "Stop a notice transaction."); 84 | } 85 | -------------------------------------------------------------------------------- /test/unit/testBroker.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | TEST(BrokerTest, Create) 7 | { 8 | auto stage = PXR_NS::UsdStage::CreateInMemory(); 9 | auto broker = unf::Broker::Create(stage); 10 | ASSERT_EQ(broker->GetStage(), stage); 11 | } 12 | 13 | TEST(BrokerTest, CreateMultiple) 14 | { 15 | auto stage = PXR_NS::UsdStage::CreateInMemory(); 16 | auto broker1 = unf::Broker::Create(stage); 17 | ASSERT_EQ(broker1->GetCurrentCount(), 2); 18 | 19 | auto broker2 = unf::Broker::Create(stage); 20 | ASSERT_EQ(broker1->GetCurrentCount(), 3); 21 | ASSERT_EQ(broker2->GetCurrentCount(), 3); 22 | 23 | // Brokers referencing the same stage are identical. 24 | ASSERT_EQ(broker1, broker2); 25 | 26 | auto otherStage = PXR_NS::UsdStage::CreateInMemory(); 27 | auto broker3 = unf::Broker::Create(otherStage); 28 | ASSERT_EQ(broker3->GetCurrentCount(), 2); 29 | 30 | // Brokers referencing distinct stages are different. 31 | ASSERT_NE(broker1, broker3); 32 | 33 | // Reference counters from previous variables did not change. 34 | ASSERT_EQ(broker1->GetCurrentCount(), 3); 35 | ASSERT_EQ(broker2->GetCurrentCount(), 3); 36 | } 37 | 38 | TEST(BrokerTest, CleanRegistry) 39 | { 40 | auto stage1 = PXR_NS::UsdStage::CreateInMemory(); 41 | auto broker1 = unf::Broker::Create(stage1); 42 | ASSERT_EQ(broker1->GetCurrentCount(), 2); 43 | 44 | // Stage is destroyed, but broker reference is kept in registry. 45 | stage1.Reset(); 46 | ASSERT_EQ(broker1->GetCurrentCount(), 2); 47 | 48 | // Registry reference is removed when a new broker is added. 49 | auto stage2 = PXR_NS::UsdStage::CreateInMemory(); 50 | unf::Broker::Create(stage2); 51 | ASSERT_EQ(broker1->GetCurrentCount(), 1); 52 | } 53 | 54 | TEST(BrokerTest, Reset) 55 | { 56 | auto stage = PXR_NS::UsdStage::CreateInMemory(); 57 | auto broker = unf::Broker::Create(stage); 58 | ASSERT_EQ(broker->GetCurrentCount(), 2); 59 | 60 | broker->Reset(); 61 | 62 | ASSERT_EQ(broker->GetCurrentCount(), 1); 63 | } 64 | 65 | TEST(BrokerTest, ResetAll) 66 | { 67 | auto stage1 = PXR_NS::UsdStage::CreateInMemory(); 68 | auto broker1 = unf::Broker::Create(stage1); 69 | ASSERT_EQ(broker1->GetCurrentCount(), 2); 70 | 71 | auto stage2 = PXR_NS::UsdStage::CreateInMemory(); 72 | auto broker2 = unf::Broker::Create(stage2); 73 | ASSERT_EQ(broker2->GetCurrentCount(), 2); 74 | 75 | auto stage3 = PXR_NS::UsdStage::CreateInMemory(); 76 | auto broker3 = unf::Broker::Create(stage3); 77 | ASSERT_EQ(broker3->GetCurrentCount(), 2); 78 | 79 | unf::Broker::ResetAll(); 80 | 81 | ASSERT_EQ(broker1->GetCurrentCount(), 1); 82 | ASSERT_EQ(broker2->GetCurrentCount(), 1); 83 | ASSERT_EQ(broker3->GetCurrentCount(), 1); 84 | } 85 | -------------------------------------------------------------------------------- /test/unit/python/test_layer_muting_changed.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pxr import Usd, Tf, Sdf 4 | import unf 5 | 6 | 7 | def test_layer_muting_changed(): 8 | """Test whether LayerMutingChanged notice is as expected.""" 9 | stage = Usd.Stage.CreateInMemory() 10 | unf.Broker.Create(stage) 11 | 12 | root_layer = stage.GetRootLayer() 13 | layer = Sdf.Layer.CreateAnonymous(".usda") 14 | root_layer.subLayerPaths.append(layer.identifier) 15 | 16 | received = [] 17 | 18 | def _validate(notice, stage): 19 | """Validate notice received.""" 20 | assert notice.IsMergeable() is True 21 | assert notice.GetTypeId() == "unf::UnfNotice::LayerMutingChanged" 22 | received.append(notice) 23 | 24 | key = Tf.Notice.Register(unf.Notice.LayerMutingChanged, _validate, stage) 25 | 26 | stage.MuteLayer(layer.identifier) 27 | 28 | # Ensure that one notice was received. 29 | assert len(received) == 1 30 | 31 | 32 | def test_layer_muting_changed_get_muted_layers(): 33 | """Ensure that expected muted layers are returned.""" 34 | stage = Usd.Stage.CreateInMemory() 35 | unf.Broker.Create(stage) 36 | 37 | root_layer = stage.GetRootLayer() 38 | layer1 = Sdf.Layer.CreateAnonymous(".usda") 39 | layer2 = Sdf.Layer.CreateAnonymous(".usda") 40 | root_layer.subLayerPaths.append(layer1.identifier) 41 | root_layer.subLayerPaths.append(layer2.identifier) 42 | 43 | received = [] 44 | 45 | def _validate(notice, stage): 46 | """Validate notice received.""" 47 | assert notice.GetMutedLayers() == [layer1.identifier, layer2.identifier] 48 | received.append(notice) 49 | 50 | key = Tf.Notice.Register(unf.Notice.LayerMutingChanged, _validate, stage) 51 | 52 | stage.MuteAndUnmuteLayers([layer1.identifier, layer2.identifier], []) 53 | 54 | # Ensure that one notice was received. 55 | assert len(received) == 1 56 | 57 | 58 | def test_layer_muting_changed_get_unmuted_layers(): 59 | """Ensure that expected unmuted layers are returned.""" 60 | stage = Usd.Stage.CreateInMemory() 61 | unf.Broker.Create(stage) 62 | 63 | root_layer = stage.GetRootLayer() 64 | layer1 = Sdf.Layer.CreateAnonymous(".usda") 65 | layer2 = Sdf.Layer.CreateAnonymous(".usda") 66 | root_layer.subLayerPaths.append(layer1.identifier) 67 | root_layer.subLayerPaths.append(layer2.identifier) 68 | 69 | stage.MuteAndUnmuteLayers([layer1.identifier, layer2.identifier], []) 70 | 71 | received = [] 72 | 73 | def _validate(notice, stage): 74 | """Validate notice received.""" 75 | assert notice.GetUnmutedLayers() == [layer1.identifier, layer2.identifier] 76 | received.append(notice) 77 | 78 | key = Tf.Notice.Register(unf.Notice.LayerMutingChanged, _validate, stage) 79 | 80 | stage.MuteAndUnmuteLayers([], [layer1.identifier, layer2.identifier]) 81 | 82 | # Ensure that one notice was received. 83 | assert len(received) == 1 84 | 85 | -------------------------------------------------------------------------------- /doc/sphinx/api_reference/python/Broker.rst: -------------------------------------------------------------------------------- 1 | ********** 2 | unf.Broker 3 | ********** 4 | 5 | .. py:class:: unf.Broker 6 | 7 | Intermediate object between the Usd Stage and any clients that needs 8 | asynchronous handling and upstream filtering of notices. 9 | 10 | .. py:staticmethod:: Create(stage) 11 | 12 | Create a broker from a Usd Stage. 13 | 14 | If a broker has already been created from this *stage*, it will be 15 | returned. Otherwise, a new one will be created and returned. 16 | 17 | Example: 18 | 19 | .. code-block:: python 20 | 21 | stage = Usd.Stage.CreateInMemory() 22 | broker = Broker(stage) 23 | 24 | :param stage: Instance of Usd Stage. 25 | 26 | :return: Instance of :class:`unf.Broker`. 27 | 28 | .. py:method:: GetStage() 29 | 30 | Return Usd Stage associated with the broker. 31 | 32 | :return: Instance of Usd Stage. 33 | 34 | .. py:method:: IsInTransaction() 35 | 36 | Indicate whether a notice transaction has been started. 37 | 38 | :return: Boolean value. 39 | 40 | .. py:method:: BeginTransaction(predicate=CapturePredicate.Default()) 41 | 42 | Start a notice transaction. 43 | 44 | Notices derived from :class:`unf.Notice.StageNotice` will be held during 45 | the transaction and emitted at the end. 46 | 47 | By default, all :class:`unf.Notice.StageNotice` notices will be 48 | captured during the entire scope of the transaction. A function 49 | *predicate* or a :class:`unf.CapturePredicate` instance can be passed to 50 | influence which notices are captured. 51 | 52 | Notices that are not captured will not be emitted. 53 | 54 | Example: 55 | 56 | .. code-block:: python 57 | 58 | # Block all notices emitted within the transaction. 59 | broker.BeginTransaction(predicate=CapturePredicate.BlockAll()) 60 | 61 | # Block all notices from type 'Foo' emitted during transaction. 62 | broker.BeginTransaction(predicate=lambda n: not isinstance(n, Foo)) 63 | 64 | .. warning:: 65 | 66 | Each transaction started must be closed with :meth:`EndTransaction`. 67 | It is preferrable to use :class:`unf.NoticeTransaction` over this 68 | API to safely manage transactions. 69 | 70 | :param predicate: Instance of :class:`unf.CapturePredicate` or function 71 | taking a :class:`unf.Notice.StageNotice` instance and returning a 72 | boolean value. By default, the :meth:`unf.apturePredicate.Default` 73 | predicate is used. 74 | 75 | .. py:method:: EndTransaction() 76 | 77 | Stop a notice transaction. 78 | 79 | This will trigger the emission of all captured 80 | :class:`unf.Notice.StageNotice` notices. Each notice type will be 81 | consolidated before emission if applicable. 82 | 83 | .. warning:: 84 | 85 | It is preferrable to use :class:`unf.NoticeTransaction` over this 86 | API to safely manage transactions. 87 | -------------------------------------------------------------------------------- /doc/sphinx/_extensions/changelog.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from docutils import nodes 4 | from docutils.parsers.rst import directives 5 | from sphinx.util.docutils import SphinxDirective 6 | 7 | 8 | class ReleaseDirective(SphinxDirective): 9 | has_content = True 10 | required_arguments = 1 11 | optional_arguments = 0 12 | option_spec = { 13 | "date": directives.unchanged_required 14 | } 15 | 16 | def run(self): 17 | """Creates a section for release notes with version and date.""" 18 | version = self.arguments[0] 19 | 20 | # Fetch today's date as default if no date is provided 21 | today_date_str = datetime.now().strftime("%Y-%m-%d") 22 | date_str = self.options.get("date", today_date_str) 23 | 24 | try: 25 | parsed_date = datetime.strptime(date_str, "%Y-%m-%d") 26 | release_date = parsed_date.strftime("%e %B %Y") 27 | except ValueError: 28 | raise ValueError(f"Invalid date format: {date_str}") 29 | 30 | # Create the version title node 31 | version_node = nodes.strong(text=version) 32 | section_title = nodes.title("", "", version_node) 33 | 34 | # Create the section node with a specific ID 35 | section_id = f"release-{version.replace(' ', '-')}" 36 | section = nodes.section( 37 | "", section_title, 38 | ids=[section_id], 39 | classes=["changelog-release"] 40 | ) 41 | 42 | # Append formatted date 43 | section.append( 44 | nodes.emphasis(text=release_date, classes=["release-date"]) 45 | ) 46 | 47 | # Parse content into a list of changes 48 | content_node = nodes.Element() 49 | self.state.nested_parse(self.content, self.content_offset, content_node) 50 | 51 | # Create a bullet list of changes 52 | changes_list = nodes.bullet_list("", classes=["changelog-change-list"]) 53 | for child in content_node: 54 | item = nodes.list_item("") 55 | item.append(child) 56 | changes_list.append(item) 57 | 58 | section.append(changes_list) 59 | 60 | return [section] 61 | 62 | 63 | class ChangeDirective(SphinxDirective): 64 | has_content = True 65 | required_arguments = 1 66 | optional_arguments = 0 67 | 68 | def run(self): 69 | """Generates a categorized list item for a changelog entry.""" 70 | category = self.arguments[0] 71 | 72 | # Create a paragraph for the category with specific styling 73 | class_name = f"changelog-category-{category.lower().replace(' ', '-')}" 74 | category_node = nodes.inline( 75 | "", category, 76 | classes=["changelog-category", class_name] 77 | ) 78 | paragraph_node = nodes.paragraph("", "", category_node) 79 | 80 | # Parse the detailed content under the category 81 | content_node = nodes.container() 82 | self.state.nested_parse(self.content, 0, content_node) 83 | paragraph_node += content_node 84 | 85 | return [paragraph_node] 86 | 87 | 88 | def setup(app): 89 | """Register extension with Sphinx.""" 90 | app.add_directive("release", ReleaseDirective) 91 | app.add_directive("change", ChangeDirective) 92 | -------------------------------------------------------------------------------- /src/unf/transaction.h: -------------------------------------------------------------------------------- 1 | #ifndef USD_NOTICE_FRAMEWORK_TRANSACTION_H 2 | #define USD_NOTICE_FRAMEWORK_TRANSACTION_H 3 | 4 | /// \file unf/transaction.h 5 | 6 | #include "unf/api.h" 7 | #include "unf/broker.h" 8 | #include "unf/capturePredicate.h" 9 | 10 | #include 11 | #include 12 | 13 | namespace unf { 14 | 15 | /// \class NoticeTransaction 16 | /// 17 | /// \brief 18 | /// Convenient [RAII](https://en.cppreference.com/w/cpp/language/raii) object 19 | /// to consolidate and filter notices derived from UnfNotice::StageNotice 20 | /// within a specific scope. 21 | class NoticeTransaction { 22 | public: 23 | /// \brief 24 | /// Create transaction from a Broker. 25 | /// 26 | /// Notices derived from UnfNotice::StageNotice will be held during 27 | /// the transaction and emitted at the end. 28 | /// 29 | /// By default, all UnfNotice::StageNotice notices will be captured during 30 | /// the entire scope of the transaction. A CapturePredicate can be passed to 31 | /// influence which notices are captured. Notices that are not captured 32 | /// will not be emitted. 33 | UNF_API NoticeTransaction( 34 | const BrokerPtr &, 35 | CapturePredicate predicate = CapturePredicate::Default()); 36 | 37 | /// \brief 38 | /// Create transaction from a Broker with a capture predicate function. 39 | /// 40 | /// The following example will filter out all 'Foo' notices emitted during 41 | /// the transaction. 42 | /// 43 | /// \code{.cpp} 44 | /// NoticeTransaction t(broker, [&](const unf::UnfNotice::StageNotice& n) { 45 | /// return (n.GetTypeId() != typeid(Foo).name()); 46 | /// }); 47 | /// \endcode 48 | UNF_API NoticeTransaction(const BrokerPtr &, const CapturePredicateFunc &); 49 | 50 | /// \brief 51 | /// Create transaction from a UsdStage. 52 | /// 53 | /// Convenient constructor to encapsulate the creation of the broker. 54 | /// 55 | /// \sa 56 | /// NoticeTransaction(const BrokerPtr &, CapturePredicate predicate = 57 | /// CapturePredicate::Default()) 58 | UNF_API NoticeTransaction( 59 | const PXR_NS::UsdStageRefPtr &, 60 | CapturePredicate predicate = CapturePredicate::Default()); 61 | 62 | /// \brief 63 | /// Create transaction from a UsdStage with a capture predicate function. 64 | /// 65 | /// Convenient constructor to encapsulate the creation of the broker. 66 | /// 67 | /// \sa 68 | /// NoticeTransaction(const BrokerPtr &, const CapturePredicateFunc&) 69 | 70 | UNF_API NoticeTransaction( 71 | const PXR_NS::UsdStageRefPtr &, const CapturePredicateFunc &); 72 | 73 | /// Delete object and end transaction. 74 | UNF_API virtual ~NoticeTransaction(); 75 | 76 | /// Remove default copy constructor. 77 | UNF_API NoticeTransaction(const NoticeTransaction &) = delete; 78 | 79 | /// Remove default assignment operator. 80 | UNF_API NoticeTransaction &operator=(const NoticeTransaction &) = delete; 81 | 82 | /// Return associated Broker instance. 83 | UNF_API BrokerPtr GetBroker() { return _broker; } 84 | 85 | private: 86 | /// Broker associated with transaction. 87 | BrokerPtr _broker; 88 | }; 89 | 90 | } // namespace unf 91 | 92 | #endif // USD_NOTICE_FRAMEWORK_TRANSACTION_H 93 | -------------------------------------------------------------------------------- /test/unit/testDispatcher.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | class DispatcherTest : public ::testing::Test { 15 | protected: 16 | using StageDispatcherPtr = PXR_NS::TfRefPtr; 17 | using NewStageDispatcherPtr = PXR_NS::TfRefPtr<::Test::NewStageDispatcher>; 18 | using TestDispatcherPtr = PXR_NS::TfRefPtr<::Test::NewDispatcher>; 19 | 20 | using Listener = ::Test::Listener< 21 | ::Test::InputNotice, ::Test::OutputNotice1, ::Test::OutputNotice2>; 22 | 23 | void SetUp() override 24 | { 25 | _stage = PXR_NS::UsdStage::CreateInMemory(); 26 | _listener.SetStage(_stage); 27 | } 28 | 29 | PXR_NS::UsdStageRefPtr _stage; 30 | Listener _listener; 31 | }; 32 | 33 | TEST_F(DispatcherTest, Original) 34 | { 35 | auto broker = unf::Broker::Create(_stage); 36 | 37 | // Ensure that the stage dispatcher type is correct. 38 | auto dispatcher = broker->GetDispatcher("StageDispatcher"); 39 | ASSERT_TRUE(PXR_NS::TfDynamic_cast(dispatcher)); 40 | 41 | // Sending InputNotice does not trigger any new notices. 42 | ::Test::InputNotice().Send(PXR_NS::TfWeakPtr(_stage)); 43 | 44 | ASSERT_EQ(_listener.Received<::Test::OutputNotice1>(), 0); 45 | ASSERT_EQ(_listener.Received<::Test::OutputNotice2>(), 0); 46 | } 47 | 48 | TEST_F(DispatcherTest, ReplaceOriginal) 49 | { 50 | auto broker = unf::Broker::Create(_stage); 51 | broker->AddDispatcher<::Test::NewStageDispatcher>(); 52 | 53 | // Ensure that stage dispacther has been replaced as expected. 54 | auto dispatcher = broker->GetDispatcher("StageDispatcher"); 55 | ASSERT_TRUE(PXR_NS::TfDynamic_cast(dispatcher)); 56 | ASSERT_FALSE(PXR_NS::TfDynamic_cast(dispatcher)); 57 | 58 | // Sending InputNotice now triggers OutputNotice1. 59 | ::Test::InputNotice().Send(PXR_NS::TfWeakPtr(_stage)); 60 | 61 | ASSERT_EQ(_listener.Received<::Test::OutputNotice1>(), 1); 62 | ASSERT_EQ(_listener.Received<::Test::OutputNotice2>(), 0); 63 | } 64 | 65 | TEST_F(DispatcherTest, AddNew) 66 | { 67 | auto broker = unf::Broker::Create(_stage); 68 | broker->AddDispatcher<::Test::NewDispatcher>(); 69 | 70 | // Ensure that stage dispacther has not been replaced. 71 | auto dispatcher1 = broker->GetDispatcher("StageDispatcher"); 72 | ASSERT_TRUE(PXR_NS::TfDynamic_cast(dispatcher1)); 73 | ASSERT_FALSE(PXR_NS::TfDynamic_cast(dispatcher1)); 74 | 75 | // Ensure that the new dispatcher is accessible from its identifier. 76 | auto dispatcher2 = broker->GetDispatcher("NewDispatcher"); 77 | ASSERT_TRUE(PXR_NS::TfDynamic_cast(dispatcher2)); 78 | 79 | // Sending the special InputNotice now triggers OutputNotice2. 80 | ::Test::InputNotice().Send(PXR_NS::TfWeakPtr(_stage)); 81 | 82 | ASSERT_EQ(_listener.Received<::Test::OutputNotice1>(), 0); 83 | ASSERT_EQ(_listener.Received<::Test::OutputNotice2>(), 1); 84 | } 85 | -------------------------------------------------------------------------------- /doc/sphinx/doxygen.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import re 3 | import subprocess 4 | import shutil 5 | 6 | ROOT = pathlib.Path().resolve() 7 | 8 | 9 | def build(): 10 | """Build API documentation.""" 11 | target = "unfApiRefDoc" 12 | 13 | build_path = ROOT / "build" 14 | output_path = ROOT / "api" 15 | 16 | # Run CMake commands 17 | subprocess.call(["cmake", "-S", ROOT, "-B", str(build_path)]) 18 | subprocess.call(["cmake", "--build", str(build_path), "--target", target]) 19 | 20 | # Remove output_path if it exists 21 | if output_path.exists() and output_path.is_dir(): 22 | shutil.rmtree(output_path) 23 | 24 | # Move doxygen API outside the build directory 25 | output_path.mkdir() 26 | 27 | shutil.move(build_path / "doxygen", output_path / "doxygen") 28 | shutil.move(build_path / "UNF.tag", output_path / "UNF.tag") 29 | 30 | return output_path 31 | 32 | 33 | def create_cmake_config(): 34 | """Create cmake config to build api docs.""" 35 | content = "\n".join([ 36 | fetch_project_content(), 37 | fetch_api_doc_content() 38 | ]) 39 | 40 | path = ROOT / "CMakeLists.txt" 41 | path.write_text(content) 42 | 43 | return path 44 | 45 | 46 | def fetch_project_content(): 47 | """Fetch info from main cmake config.""" 48 | # cmake version available on RTD via apt install is lower than 3.20, 49 | # so we hard-code it for now. 50 | content = "cmake_minimum_required(VERSION 3.15)\n\n" 51 | 52 | path = ROOT.parent.parent / "CMakeLists.txt" 53 | data = path.read_text() 54 | 55 | patterns = [ 56 | r"project\(.*?\)", 57 | r"find_package\(\s*Doxygen.*?\)", 58 | ] 59 | 60 | for pattern in patterns: 61 | match = re.search(pattern, data, re.MULTILINE | re.DOTALL) 62 | if not match: 63 | raise ValueError(f"Pattern not found: {pattern!r}") 64 | 65 | content += f"{match.group()}\n\n" 66 | 67 | return content 68 | 69 | 70 | def fetch_api_doc_content(): 71 | """Fetch info from documentation cmake config.""" 72 | content = "" 73 | 74 | path = ROOT.parent / "CMakeLists.txt" 75 | data = path.read_text() 76 | 77 | # Extract DOXYGEN-related settings 78 | options = re.findall(r"set\(\s*DOXYGEN.*?\s*\)", data, re.MULTILINE | re.DOTALL) 79 | content += "\n".join(options) + "\n" 80 | 81 | # Update path to tag file. 82 | content = re.sub("/doc/", "/../", content) 83 | 84 | # Update doxygen html output path. 85 | content = re.sub( 86 | r"(set\(\s*DOXYGEN_HTML_OUTPUT )(.*?)(\s*\))", 87 | r'\g<1>"doxygen"\g<3>', 88 | content, re.MULTILINE | re.DOTALL 89 | ) 90 | 91 | # Update tag file path. 92 | content = re.sub( 93 | r"(set\(\s*DOXYGEN_GENERATE_TAGFILE )(.*?)(\s*\))", 94 | r'\g<1>"${CMAKE_CURRENT_BINARY_DIR}/UNF.tag"\g<3>', 95 | content, re.MULTILINE | re.DOTALL 96 | ) 97 | 98 | match = re.search( 99 | r"doxygen_add_docs\(.*?\)", data, 100 | re.MULTILINE | re.DOTALL 101 | ) 102 | if not match: 103 | raise ValueError("No command found to build doxygen docs") 104 | 105 | # Update doxygen input paths. 106 | command = re.sub("/doc/", "/../", match.group()) 107 | command = re.sub("/src/", "/../../src/", command) 108 | 109 | content += f"\n{command}" 110 | return content 111 | -------------------------------------------------------------------------------- /doc/sphinx/glossary.rst: -------------------------------------------------------------------------------- 1 | ******** 2 | Glossary 3 | ******** 4 | 5 | .. glossary:: 6 | 7 | Boost 8 | Set of C++ libraries providing features complementary from the standard 9 | library. 10 | 11 | .. seealso:: https://www.boost.org/ 12 | 13 | Boost Python 14 | Component library from :term:`Boost` which is a framework for 15 | interfacing Python and C++. 16 | 17 | .. seealso:: https://wiki.python.org/moin/boost.python 18 | 19 | Clang-format 20 | Tool to format C/C++ code according to a set of rules and heuristics. 21 | 22 | .. seealso:: https://clang.llvm.org/docs/ClangFormat.html 23 | 24 | CMake 25 | Compilation configuration platform that manages the build process in an 26 | operating system and in a compiler-independent manner. 27 | 28 | .. seealso:: https://cmake.org/overview/ 29 | 30 | CTest 31 | CTest is an executable that comes with :term:`CMake` which handles 32 | running the tests. 33 | 34 | .. seealso:: https://cmake.org/cmake/help/latest/manual/ctest.1.html 35 | 36 | Doxygen 37 | Documentation generator and static analysis tool which supports many 38 | languages, including C and C++. 39 | 40 | .. seealso:: https://doxygen.nl/ 41 | 42 | GTest 43 | Google Test is a testing and mocking framework for C++. 44 | 45 | .. seealso:: http://google.github.io/googletest/ 46 | 47 | Plug 48 | Plugin framework included with the :term:`USD` API. 49 | 50 | .. seealso:: https://graphics.pixar.com/usd/release/api/plug_page_front.html 51 | 52 | Presto Animation 53 | Presto is the proprietary software developed and used in-house by Pixar 54 | Animation Studios in the animation of its features and short films. 55 | 56 | .. seealso:: https://en.wikipedia.org/wiki/Presto_(animation_software) 57 | 58 | Pytest 59 | Python testing framework. 60 | 61 | .. seealso:: https://docs.pytest.org/ 62 | 63 | reStructuredText 64 | Lightweight markup language. 65 | 66 | .. seealso:: https://docutils.sourceforge.io/rst.html 67 | 68 | SDF 69 | Scene Description Foundations (SDF) provides the foundations for 70 | serializing scene description and provides the primitive abstractions 71 | for interacting with it. 72 | 73 | .. seealso:: https://graphics.pixar.com/usd/release/api/sdf_page_front.html 74 | 75 | Sphinx 76 | Python documentation generator which converts :term:`reStructuredText` 77 | files into HTML and other formats. 78 | 79 | .. seealso:: https://www.sphinx-doc.org/ 80 | 81 | TBB 82 | C++ template library developed by Intel for parallel programming on 83 | multi-core processors. 84 | 85 | .. seealso:: https://oneapi-src.github.io/oneTBB/ 86 | 87 | Tf Notification System 88 | Event Notification system included in the :term:`USD` library as part of 89 | the Tool Foundations (tf) module. It follows the `Observer pattern 90 | `_. 91 | 92 | .. seealso:: https://graphics.pixar.com/usd/release/api/page_tf__notification.html 93 | 94 | USD 95 | Universal Scene Description (USD) is a framework for interchange of 96 | 3D computer graphics data which focuses on collaboration and 97 | non-destructive editing. 98 | 99 | .. seealso:: https://graphics.pixar.com/usd/release/intro.html 100 | -------------------------------------------------------------------------------- /cmake/modules/FindUSD.cmake: -------------------------------------------------------------------------------- 1 | # Discover required Pixar USD targets. 2 | # 3 | # This module defines the following imported targets: 4 | # usd::usd 5 | # usd::sdf 6 | # usd::tf 7 | # usd::plug 8 | # usd::arch 9 | # usd::vt 10 | # 11 | # Usage: 12 | # find_package(USD) 13 | # find_package(USD REQUIRED) 14 | # find_package(USD 0.20.11 REQUIRED) 15 | # 16 | # Note: 17 | # The USD_ROOT environment variable or CMake variable can be used to 18 | # prepend a custom search path. 19 | # (https://cmake.org/cmake/help/latest/policy/CMP0074.html) 20 | # 21 | # The PXR_LIB_PREFIX option can be used to indicate the expected 22 | # library prefix. By default, it will look for "libusd_" and "lib". 23 | # (https://github.com/PixarAnimationStudios/OpenUSD/blob/release/BUILDING.md) 24 | # 25 | # We do not use pxrConfig.cmake to keep compatibility with USD included 26 | # within Presto. 27 | 28 | include(FindPackageHandleStandardArgs) 29 | 30 | if (NOT DEFINED PXR_LIB_PREFIX) 31 | set(PXR_LIB_PREFIX "usd_") 32 | endif() 33 | 34 | find_path( 35 | USD_INCLUDE_DIR 36 | pxr/pxr.h 37 | PATH_SUFFIXES 38 | include 39 | ) 40 | 41 | set(USD_LIBRARIES usd sdf tf plug arch vt boost python) 42 | 43 | mark_as_advanced(USD_INCLUDE_DIR USD_LIBRARIES) 44 | 45 | foreach(NAME IN LISTS USD_LIBRARIES) 46 | find_library( 47 | "${NAME}_LIBRARY" 48 | NAMES 49 | ${PXR_LIB_PREFIX}${NAME} 50 | ${NAME} 51 | PATH_SUFFIXES 52 | ${CMAKE_INSTALL_LIBDIR} 53 | lib 54 | ) 55 | 56 | get_filename_component(${NAME}_LIBRARY "${${NAME}_LIBRARY}" REALPATH) 57 | 58 | mark_as_advanced("${NAME}_LIBRARY") 59 | endforeach() 60 | 61 | 62 | if(USD_INCLUDE_DIR AND EXISTS "${USD_INCLUDE_DIR}/pxr/pxr.h") 63 | file(READ "${USD_INCLUDE_DIR}/pxr/pxr.h" _pxr_header) 64 | foreach(label MAJOR MINOR PATCH) 65 | string( 66 | REGEX REPLACE ".*#define PXR_${label}_VERSION ([0-9]+).*" "\\1" 67 | _pxr_${label} "${_pxr_header}" 68 | ) 69 | endforeach() 70 | 71 | set(USD_VERSION ${_pxr_MAJOR}.${_pxr_MINOR}.${_pxr_PATCH}) 72 | 73 | set(USD_DEPENDENCIES "TBB::tbb") 74 | if (BUILD_PYTHON_BINDINGS) 75 | list(APPEND USD_DEPENDENCIES "Python::Python") 76 | endif() 77 | 78 | # Detect whether PXR_USE_INTERNAL_BOOST_PYTHON is explicitly enabled 79 | set(USD_USE_INTERNAL_BOOST_PYTHON ON CACHE INTERNAL "") 80 | string(REGEX MATCH 81 | "#if +0[^\n]*\n[ \t]*#define +PXR_USE_INTERNAL_BOOST_PYTHON" 82 | _use_external_boost_python "${_pxr_header}") 83 | 84 | # Use external Boost dependencies if USD version is less than 0.25.5, and 85 | # if internal Boost.Python is not explicitly disabled 86 | if (USD_VERSION VERSION_LESS "0.25.5" OR _use_external_boost_python) 87 | set(USD_USE_INTERNAL_BOOST_PYTHON OFF CACHE INTERNAL "") 88 | if (BUILD_PYTHON_BINDINGS) 89 | list(APPEND USD_DEPENDENCIES "Boost::boost" "Boost::python") 90 | endif() 91 | endif() 92 | 93 | mark_as_advanced( 94 | _pxr_MAJOR 95 | _pxr_MINOR 96 | _pxr_PATCH 97 | _use_external_boost_python 98 | USD_VERSION 99 | USD_DEPENDENCIES 100 | ) 101 | endif() 102 | 103 | find_package_handle_standard_args( 104 | USD 105 | REQUIRED_VARS 106 | USD_INCLUDE_DIR 107 | usd_LIBRARY 108 | sdf_LIBRARY 109 | tf_LIBRARY 110 | plug_LIBRARY 111 | arch_LIBRARY 112 | vt_LIBRARY 113 | VERSION_VAR 114 | USD_VERSION 115 | ) 116 | 117 | if (USD_FOUND) 118 | foreach(NAME IN LISTS USD_LIBRARIES) 119 | if (NOT TARGET usd::${NAME}) 120 | add_library(usd::${NAME} UNKNOWN IMPORTED) 121 | set_target_properties(usd::${NAME} PROPERTIES 122 | IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" 123 | INTERFACE_LINK_LIBRARIES "${USD_DEPENDENCIES}" 124 | IMPORTED_LOCATION "${${NAME}_LIBRARY}" 125 | INTERFACE_INCLUDE_DIRECTORIES "${USD_INCLUDE_DIR}" 126 | ) 127 | endif() 128 | endforeach() 129 | endif() 130 | -------------------------------------------------------------------------------- /src/python/wrapTransaction.cpp: -------------------------------------------------------------------------------- 1 | // clang-format off 2 | 3 | #include "./predicate.h" 4 | 5 | #include "unf/broker.h" 6 | #include "unf/capturePredicate.h" 7 | #include "unf/transaction.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #ifndef PXR_USE_INTERNAL_BOOST_PYTHON 15 | #include 16 | #include 17 | using namespace boost::python; 18 | #else 19 | #include 20 | #include 21 | using namespace PXR_BOOST_PYTHON_NAMESPACE; 22 | #endif 23 | 24 | using namespace unf; 25 | 26 | PXR_NAMESPACE_USING_DIRECTIVE 27 | 28 | // Expose C++ RAII class as python context manager. 29 | struct PythonNoticeTransaction { 30 | PythonNoticeTransaction( 31 | const BrokerWeakPtr& broker, const _CapturePredicateFunc& func) 32 | : _func(func) 33 | { 34 | _makeContext = [=]() { 35 | return new NoticeTransaction(broker, WrapPredicate(_func)); 36 | }; 37 | } 38 | 39 | PythonNoticeTransaction( 40 | const BrokerWeakPtr& broker, CapturePredicate predicate) 41 | : _predicate(predicate) 42 | { 43 | _makeContext = [=]() { 44 | return new NoticeTransaction(broker, _predicate); 45 | }; 46 | } 47 | 48 | PythonNoticeTransaction( 49 | const UsdStageWeakPtr& stage, const _CapturePredicateFunc& func) 50 | : _func(func) 51 | { 52 | _makeContext = [=]() { 53 | return new NoticeTransaction(stage, WrapPredicate(_func)); 54 | }; 55 | } 56 | 57 | PythonNoticeTransaction( 58 | const UsdStageWeakPtr& stage, CapturePredicate predicate) 59 | : _predicate(predicate) 60 | { 61 | _makeContext = [=]() { 62 | return new NoticeTransaction(stage, _predicate); 63 | }; 64 | } 65 | 66 | // Instantiate the C++ class object and hold it by shared_ptr. 67 | PythonNoticeTransaction const* __enter__() 68 | { 69 | _context.reset(_makeContext()); 70 | return this; 71 | } 72 | 73 | // Drop the shared_ptr. 74 | void __exit__(object, object, object) { _context.reset(); } 75 | 76 | BrokerPtr GetBroker() { return _context->GetBroker(); } 77 | 78 | private: 79 | std::shared_ptr _context; 80 | std::function _makeContext; 81 | 82 | _CapturePredicateFunc _func = nullptr; 83 | CapturePredicate _predicate = CapturePredicate::Default(); 84 | }; 85 | 86 | void wrapTransaction() 87 | { 88 | // Ensure that predicate function can be passed from Python. 89 | TfPyFunctionFromPython<_CapturePredicateFuncRaw>(); 90 | 91 | class_( 92 | "NoticeTransaction", 93 | "Context manager object which consolidates and filter notices derived " 94 | "from UnfNotice.StageNotice within a specific scope", 95 | no_init) 96 | 97 | .def(init( 98 | (arg("broker"), arg("predicate") = CapturePredicate::Default()), 99 | "Create transaction from a Broker.")) 100 | 101 | .def(init( 102 | (arg("broker"), arg("predicate")), 103 | "Create transaction from a Broker with a capture predicate " 104 | "function.")) 105 | 106 | .def(init( 107 | (arg("stage"), arg("predicate") = CapturePredicate::Default()), 108 | "Create transaction from a UsdStage.")) 109 | 110 | .def(init( 111 | (arg("stage"), arg("predicate")), 112 | "Create transaction from a UsdStage with a capture predicate " 113 | "function.")) 114 | 115 | .def( 116 | "__enter__", 117 | &PythonNoticeTransaction::__enter__, 118 | return_internal_reference<>()) 119 | 120 | .def("__exit__", &PythonNoticeTransaction::__exit__) 121 | 122 | .def( 123 | "GetBroker", 124 | &PythonNoticeTransaction::GetBroker, 125 | "Return associated Broker instance.", 126 | return_value_policy()); 127 | } 128 | -------------------------------------------------------------------------------- /.github/workflows/test-linux.yml: -------------------------------------------------------------------------------- 1 | name: test-linux 2 | 3 | on: 4 | push: 5 | branches: [ main, dev ] 6 | pull_request: 7 | branches: [ main, dev ] 8 | 9 | # Run tests once a week on Sunday. 10 | schedule: 11 | - cron: "0 6 * * 0" 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | test-linux: 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 300 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | usd: ["v24.08", "v25.05"] 25 | python: ["", "3.10", "3.12"] 26 | 27 | name: "USD-${{ matrix.usd }}${{ matrix.python != '' && format('-py{0}', matrix.python) || '-no-py' }}" 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | - name: Set up Python ${{ matrix.python }} 33 | if: matrix.python != '' 34 | uses: actions/setup-python@v5 35 | with: 36 | python-version: ${{ matrix.python }} 37 | 38 | - name: Create Build Environment 39 | run: | 40 | sudo apt update 41 | sudo apt install -y libgtest-dev ninja-build 42 | mkdir -p ${{github.workspace}}/build 43 | mkdir -p ${{runner.temp}}/USD 44 | 45 | - name: Install test Python deps 46 | if: matrix.python != '' 47 | run: | 48 | python -m pip install -r ${{github.workspace}}/test/requirements.txt 49 | 50 | - name: Download USD 51 | working-directory: ${{runner.temp}}/USD 52 | run: | 53 | git clone https://github.com/PixarAnimationStudios/OpenUSD.git \ 54 | --depth 1 --branch ${{ matrix.usd }} ./src 55 | 56 | - name: Apply patch for USD v24.08 57 | if: matrix.usd == 'v24.08' 58 | working-directory: ${{runner.temp}}/USD 59 | run: | 60 | sed -i '/BOOST_URL/ s|boostorg.jfrog.io.*/release|sourceforge.net/projects/boost/files/boost|' src/build_scripts/build_usd.py 61 | sed -i '/BOOST_URL/ s|source/boost|boost|' src/build_scripts/build_usd.py 62 | sed -i '/BOOST_URL/ s|\.zip"|.zip/download"|' src/build_scripts/build_usd.py 63 | 64 | - name: Install USD 65 | if: matrix.python != '' 66 | working-directory: ${{runner.temp}}/USD 67 | run: | 68 | python ./src/build_scripts/build_usd.py . \ 69 | --no-tests \ 70 | --no-examples \ 71 | --no-tutorials \ 72 | --no-tools \ 73 | --no-docs \ 74 | --no-imaging \ 75 | --no-materialx \ 76 | -v 77 | 78 | - name: Install USD (without Python) 79 | if: matrix.python == '' 80 | working-directory: ${{runner.temp}}/USD 81 | run: | 82 | python ./src/build_scripts/build_usd.py . \ 83 | --no-tests \ 84 | --no-examples \ 85 | --no-tutorials \ 86 | --no-tools \ 87 | --no-docs \ 88 | --no-imaging \ 89 | --no-materialx \ 90 | --no-python \ 91 | -v 92 | 93 | - name: Configure & Build 94 | if: matrix.python != '' 95 | working-directory: ${{github.workspace}}/build 96 | run: | 97 | export PYTHONPATH="${{runner.temp}}/USD/lib/python" 98 | cmake \ 99 | -D "BUILD_DOCS=OFF" \ 100 | -D "CMAKE_INCLUDE_PATH=${{runner.temp}}/USD/include" \ 101 | -D "CMAKE_LIBRARY_PATH=${{runner.temp}}/USD/lib" \ 102 | .. 103 | cmake --build . --config Release 104 | 105 | - name: Configure & Build (without Python) 106 | if: matrix.python == '' 107 | working-directory: ${{github.workspace}}/build 108 | run: | 109 | cmake \ 110 | -D "BUILD_DOCS=OFF" \ 111 | -D "BUILD_PYTHON_BINDINGS=OFF" \ 112 | -D "CMAKE_INCLUDE_PATH=${{runner.temp}}/USD/include" \ 113 | -D "CMAKE_LIBRARY_PATH=${{runner.temp}}/USD/lib" \ 114 | .. 115 | cmake --build . --config Release 116 | 117 | - name: Check for formatting errors 118 | working-directory: ${{github.workspace}}/build 119 | run: | 120 | cmake --build . --target format 121 | STATUS_OUTPUT=$(git -C .. status --porcelain) 122 | if [ -n "$STATUS_OUTPUT" ]; then 123 | echo "Code formatting errors found:" 124 | git -C .. diff 125 | exit 1 126 | else 127 | echo "No formatting errors found." 128 | fi 129 | 130 | - name: Run Test 131 | working-directory: ${{github.workspace}}/build 132 | run: ctest -VV 133 | env: 134 | CTEST_OUTPUT_ON_FAILURE: True 135 | 136 | -------------------------------------------------------------------------------- /src/python/wrapNotice.cpp: -------------------------------------------------------------------------------- 1 | // clang-format off 2 | 3 | #include "unf/notice.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #ifndef PXR_USE_INTERNAL_BOOST_PYTHON 13 | #include 14 | using namespace boost::python; 15 | #else 16 | #include 17 | using namespace PXR_BOOST_PYTHON_NAMESPACE; 18 | #endif 19 | 20 | using namespace unf::UnfNotice; 21 | 22 | PXR_NAMESPACE_USING_DIRECTIVE 23 | 24 | namespace { 25 | 26 | TF_INSTANTIATE_NOTICE_WRAPPER(StageNotice, TfNotice); 27 | TF_INSTANTIATE_NOTICE_WRAPPER(StageContentsChanged, StageNotice); 28 | TF_INSTANTIATE_NOTICE_WRAPPER(ObjectsChanged, StageNotice); 29 | TF_INSTANTIATE_NOTICE_WRAPPER(StageEditTargetChanged, StageNotice); 30 | TF_INSTANTIATE_NOTICE_WRAPPER(LayerMutingChanged, StageNotice); 31 | 32 | } // anonymous namespace 33 | 34 | // Dummy class to reproduce namespace in Python. 35 | class PythonUnfNotice {}; 36 | 37 | void wrapNotice() 38 | { 39 | scope s = class_( 40 | "Notice", 41 | "Regroup all standalone notices used by the library.", 42 | no_init); 43 | 44 | TfPyNoticeWrapper::Wrap() 45 | .def( 46 | "IsMergeable", 47 | &StageNotice::IsMergeable, 48 | "Indicate whether notice from the same type can be consolidated " 49 | "during a transaction") 50 | 51 | .def( 52 | "GetTypeId", 53 | &StageNotice::GetTypeId, 54 | "Return unique type identifier"); 55 | 56 | TfPyNoticeWrapper::Wrap(); 57 | 58 | TfPyNoticeWrapper::Wrap() 59 | .def( 60 | "AffectedObject", 61 | &ObjectsChanged::AffectedObject, 62 | "Indicate whether object was affected by the change that generated " 63 | "this notice.") 64 | 65 | .def( 66 | "ResyncedObject", 67 | &ObjectsChanged::ResyncedObject, 68 | "Indicate whether object was resynced by the change that generated " 69 | "this notice.") 70 | 71 | .def( 72 | "ChangedInfoOnly", 73 | &ObjectsChanged::ChangedInfoOnly, 74 | "Indicate whether object was modified but not resynced by the " 75 | "change that generated this notice.") 76 | 77 | .def( 78 | "GetResyncedPaths", 79 | &ObjectsChanged::GetResyncedPaths, 80 | "Return list of paths that are resynced in lexicographical order.", 81 | return_value_policy()) 82 | 83 | .def( 84 | "GetChangedInfoOnlyPaths", 85 | &ObjectsChanged::GetChangedInfoOnlyPaths, 86 | "Return list of paths that are modified but not resynced in " 87 | "lexicographical order.", 88 | return_value_policy()) 89 | 90 | .def( 91 | "GetChangedFields", 92 | (unf::TfTokenSet(ObjectsChanged::*)(const SdfPath&) const) 93 | & ObjectsChanged::GetChangedFields, 94 | "Return the list of changed fields in layers that affected the " 95 | "path", 96 | return_value_policy()) 97 | 98 | .def( 99 | "GetChangedFields", 100 | (unf::TfTokenSet(ObjectsChanged::*)(const UsdObject&) const) 101 | & ObjectsChanged::GetChangedFields, 102 | "Return the list of changed fields in layers that affected the " 103 | "object", 104 | return_value_policy()) 105 | 106 | .def( 107 | "HasChangedFields", 108 | (bool(ObjectsChanged::*)(const SdfPath&) const) 109 | & ObjectsChanged::HasChangedFields, 110 | "Indicate whether any changed fields affected the path") 111 | 112 | .def( 113 | "HasChangedFields", 114 | (bool(ObjectsChanged::*)(const UsdObject&) const) 115 | & ObjectsChanged::HasChangedFields, 116 | "Indicate whether any changed fields affected the object"); 117 | 118 | TfPyNoticeWrapper::Wrap(); 119 | 120 | TfPyNoticeWrapper::Wrap() 121 | .def( 122 | "GetMutedLayers", 123 | &LayerMutingChanged::GetMutedLayers, 124 | "Returns identifiers of the layers that were muted.", 125 | return_value_policy()) 126 | 127 | .def( 128 | "GetUnmutedLayers", 129 | &LayerMutingChanged::GetUnmutedLayers, 130 | "Returns identifiers of the layers that were unmuted.", 131 | return_value_policy()); 132 | } 133 | -------------------------------------------------------------------------------- /test/integration/python/test_change_edit_target.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pxr import Usd, Sdf, Tf 4 | import unf 5 | 6 | import pytest 7 | 8 | 9 | @pytest.mark.parametrize("notice_type, excepted", [ 10 | ("StageNotice", 2), 11 | ("StageContentsChanged", 0), 12 | ("ObjectsChanged", 0), 13 | ("StageEditTargetChanged", 2), 14 | ("LayerMutingChanged", 0), 15 | ], ids=[ 16 | "StageNotice", 17 | "StageContentsChanged", 18 | "ObjectsChanged", 19 | "StageEditTargetChanged", 20 | "LayerMutingChanged", 21 | ]) 22 | def test_change_edit_target(notice_type, excepted, stage_with_layers): 23 | """Change edit target. 24 | """ 25 | stage = stage_with_layers 26 | unf.Broker.Create(stage) 27 | 28 | # Listen to broker notice. 29 | received_broker = [] 30 | key1 = Tf.Notice.Register( 31 | getattr(unf.Notice, notice_type), 32 | lambda n, _: received_broker.append(n), stage) 33 | 34 | # Listen to corresponding USD notice. 35 | received_usd = [] 36 | key2 = Tf.Notice.Register( 37 | getattr(Usd.Notice, notice_type), 38 | lambda n, _: received_usd.append(n), stage) 39 | 40 | layers = stage.GetRootLayer().subLayerPaths 41 | layer1 = Sdf.Layer.Find(layers[0]) 42 | layer2 = Sdf.Layer.Find(layers[1]) 43 | 44 | stage.SetEditTarget(Usd.EditTarget(layer1)) 45 | stage.SetEditTarget(Usd.EditTarget(layer2)) 46 | 47 | # Ensure that we received the same number of notices. 48 | assert len(received_broker) == excepted 49 | assert len(received_usd) == excepted 50 | 51 | @pytest.mark.parametrize("notice_type, expected_usd, expected_broker", [ 52 | ("StageNotice", 2, 1), 53 | ("StageContentsChanged", 0, 0), 54 | ("ObjectsChanged", 0, 0), 55 | ("StageEditTargetChanged", 2, 1), 56 | ("LayerMutingChanged", 0, 0), 57 | ], ids=[ 58 | "StageNotice", 59 | "StageContentsChanged", 60 | "ObjectsChanged", 61 | "StageEditTargetChanged", 62 | "LayerMutingChanged", 63 | ]) 64 | def test_change_edit_target_batching( 65 | notice_type, expected_usd, expected_broker, stage_with_layers 66 | ): 67 | """Change edit target and batch broker notices. 68 | """ 69 | stage = stage_with_layers 70 | broker = unf.Broker.Create(stage) 71 | 72 | # Listen to broker notice. 73 | received_broker = [] 74 | key1 = Tf.Notice.Register( 75 | getattr(unf.Notice, notice_type), 76 | lambda n, _: received_broker.append(n), stage) 77 | 78 | # Listen to corresponding USD notice. 79 | received_usd = [] 80 | key2 = Tf.Notice.Register( 81 | getattr(Usd.Notice, notice_type), 82 | lambda n, _: received_usd.append(n), stage) 83 | 84 | broker.BeginTransaction() 85 | 86 | layers = stage.GetRootLayer().subLayerPaths 87 | layer1 = Sdf.Layer.Find(layers[0]) 88 | layer2 = Sdf.Layer.Find(layers[1]) 89 | 90 | stage.SetEditTarget(Usd.EditTarget(layer1)) 91 | stage.SetEditTarget(Usd.EditTarget(layer2)) 92 | 93 | # Ensure that broker notices are not sent during a transaction. 94 | assert len(received_broker) == 0 95 | 96 | # While USD Notices are being sent as expected. 97 | assert len(received_usd) == expected_usd 98 | 99 | broker.EndTransaction() 100 | 101 | # Ensure that consolidated broker notices are sent after a transaction. 102 | assert len(received_broker) == expected_broker 103 | 104 | @pytest.mark.parametrize("notice_type, expected_usd", [ 105 | ("StageNotice", 2), 106 | ("StageContentsChanged", 0), 107 | ("ObjectsChanged", 0), 108 | ("StageEditTargetChanged", 2), 109 | ("LayerMutingChanged", 0), 110 | ], ids=[ 111 | "StageNotice", 112 | "StageContentsChanged", 113 | "ObjectsChanged", 114 | "StageEditTargetChanged", 115 | "LayerMutingChanged", 116 | ]) 117 | def test_change_edit_target_blocking( 118 | notice_type, expected_usd, stage_with_layers 119 | ): 120 | """Change edit target and block broker notices. 121 | """ 122 | stage = stage_with_layers 123 | broker = unf.Broker.Create(stage) 124 | 125 | # Listen to broker notice. 126 | received_broker = [] 127 | key1 = Tf.Notice.Register( 128 | getattr(unf.Notice, notice_type), 129 | lambda n, _: received_broker.append(n), stage) 130 | 131 | # Listen to corresponding USD notice. 132 | received_usd = [] 133 | key2 = Tf.Notice.Register( 134 | getattr(Usd.Notice, notice_type), 135 | lambda n, _: received_usd.append(n), stage) 136 | 137 | # Predicate blocking all broker notices. 138 | broker.BeginTransaction(predicate=lambda _: False) 139 | 140 | layers = stage.GetRootLayer().subLayerPaths 141 | layer1 = Sdf.Layer.Find(layers[0]) 142 | layer2 = Sdf.Layer.Find(layers[1]) 143 | 144 | stage.SetEditTarget(Usd.EditTarget(layer1)) 145 | stage.SetEditTarget(Usd.EditTarget(layer2)) 146 | 147 | # Ensure that broker notices are not sent during a transaction. 148 | assert len(received_broker) == 0 149 | 150 | # While USD Notices are being sent as expected. 151 | assert len(received_usd) == expected_usd 152 | 153 | broker.EndTransaction() 154 | 155 | # Ensure that no broker notices have been received. 156 | assert len(received_broker) == 0 157 | -------------------------------------------------------------------------------- /doc/sphinx/dispatchers.rst: -------------------------------------------------------------------------------- 1 | .. _dispatchers: 2 | 3 | ***************** 4 | Using Dispatchers 5 | ***************** 6 | 7 | As mentioned :ref:`previously `, in order to capture standalone 8 | notices during a transaction, it needs to be sent via the :unf-cpp:`Broker`. 9 | owever, notices are rarely sent directly within the transaction like in our 10 | examples. Most of the time, we want standalone notices to be triggered by other 11 | incoming notices. 12 | 13 | This is therefore preferable to send standalone notices using a 14 | :unf-cpp:`Dispatcher`. A dispatcher is an object which triggers :ref:`notices 15 | ` when incoming notices are received. It can be retrieved from the 16 | :unf-cpp:`Broker` via its identifier: 17 | 18 | .. code-block:: cpp 19 | 20 | auto stage = PXR_NS::UsdStage::CreateInMemory(); 21 | auto broker = unf::Broker::Create(stage); 22 | auto dispatcher = broker->GetDispatcher("..."); 23 | 24 | Listeners registered by any dispatcher can be revoked and re-registered as 25 | follows: 26 | 27 | .. code-block:: cpp 28 | 29 | // Revoke all registered listeners. 30 | dispatcher->Revoke(); 31 | 32 | // Re-register listeners. 33 | dispatcher->Register(); 34 | 35 | .. warning:: 36 | 37 | Dispatchers cannot be manipulated in Python. 38 | 39 | .. _dispatchers/stage: 40 | 41 | Stage Dispatcher 42 | ================ 43 | 44 | By default, the :unf-cpp:`Broker` only contains the :unf-cpp:`StageDispatcher`, 45 | which is in charge of emitting a :ref:`default standalone notice 46 | ` for each :term:`USD` notice received. 47 | 48 | Its identifier is "StageDispatcher": 49 | 50 | .. code-block:: cpp 51 | 52 | auto stage = PXR_NS::UsdStage::CreateInMemory(); 53 | auto broker = unf::Broker::Create(stage); 54 | auto dispatcher = broker->GetDispatcher("StageDispatcher"); 55 | 56 | .. note:: 57 | 58 | The Stage Dispatcher can be overriden by adding a new dispatcher with the 59 | same identifier. 60 | 61 | .. _dispatchers/create: 62 | 63 | Creating a Dispatcher 64 | ===================== 65 | 66 | The :unf-cpp:`Dispatcher` interface can be used to create a new dispatcher. The 67 | "Register" method must be implemented to register the listeners in charge of 68 | emitting the new notices. 69 | 70 | A convenient "_Register" protected method is provided to trigger a new notice 71 | from one specific notice type. The new notice will be created by passing the 72 | notice received to its constructor: 73 | 74 | .. code-block:: cpp 75 | 76 | class NewDispatcher : public unf::Dispatcher { 77 | public: 78 | NewDispatcher(const unf::BrokerWeakPtr& broker) 79 | : unf::Dispatcher(broker) {} 80 | 81 | std::string GetIdentifier() const override { return "NewDispatcher"; }; 82 | 83 | void Register() { 84 | // Register listener to create and emit 'OutputNotice' when 85 | // 'InputNotice' is received. 86 | _Register(); 87 | } 88 | }; 89 | 90 | Otherwise, the listener can be registered as follows: 91 | 92 | .. code-block:: cpp 93 | 94 | class NewDispatcher : public unf::Dispatcher { 95 | public: 96 | NewDispatcher(const unf::BrokerWeakPtr& broker) 97 | : unf::Dispatcher(broker) {} 98 | 99 | std::string GetIdentifier() const override { return "NewDispatcher"; }; 100 | 101 | void Register() { 102 | auto self = PXR_NS::TfCreateWeakPtr(this); 103 | auto cb = &NewDispatcher::_OnReceiving; 104 | _keys.push_back( 105 | PXR_NS::TfNotice::Register(self, cb, _broker->GetStage())); 106 | } 107 | 108 | void _OnReceiving(const InputNotice& notice) 109 | { 110 | _broker->Send(/* arguments to create notice */); 111 | } 112 | 113 | }; 114 | 115 | The new dispatcher can be added to the :unf-cpp:`Broker` as follows: 116 | 117 | .. code-block:: cpp 118 | 119 | auto stage = PXR_NS::UsdStage::CreateInMemory(); 120 | auto broker = unf::Broker::Create(stage); 121 | 122 | broker->AddDispatcher(); 123 | 124 | .. _dispatchers/plugin: 125 | 126 | Creating a plugin 127 | ================= 128 | 129 | The new dispatcher can be automatically discovered and registered when the 130 | :unf-cpp:`Broker` is created using the :term:`Plug` framework. 131 | 132 | First, a corresponding runtime TfType must be defined: 133 | 134 | .. code-block:: cpp 135 | 136 | TF_REGISTRY_FUNCTION(TfType) 137 | { 138 | unf::DispatcherDefine(); 139 | } 140 | 141 | Then a :file:`plugInfo.json` configuration must be created. It should be in 142 | the form of: 143 | 144 | .. code-block:: json 145 | 146 | { 147 | "Plugins": [ 148 | { 149 | "Info": { 150 | "Types" : { 151 | "NewDispatcher" : { 152 | "bases": [ "Dispatcher" ] 153 | } 154 | } 155 | }, 156 | "LibraryPath": "libNewDispatcher.so", 157 | "Name": "NewDispatcher", 158 | "Type": "library" 159 | } 160 | ] 161 | } 162 | 163 | The path to this configuration file must be included in the 164 | :envvar:`PXR_PLUGINPATH_NAME` environment variable. 165 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | if(WIN32) 2 | cmake_minimum_required(VERSION 3.27) 3 | else() 4 | cmake_minimum_required(VERSION 3.20) 5 | endif() 6 | 7 | project(unf 8 | VERSION 0.9.0 9 | LANGUAGES CXX 10 | ) 11 | 12 | include(GNUInstallDirs) 13 | 14 | if (DEFINED ENV{CXXFLAGS_STD}) 15 | string(SUBSTRING "$ENV{CXXFLAGS_STD}" 3 -1 cxx_std) 16 | else() 17 | set(cxx_std 17) 18 | endif() 19 | set(CMAKE_CXX_STANDARD "${cxx_std}" CACHE STRING "Default C++ standard") 20 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 21 | set(CMAKE_CXX_EXTENSIONS OFF) 22 | 23 | if (MSVC) 24 | # Make sure WinDef.h does not define min and max macros which 25 | # will conflict with std::min() and std::max(). 26 | add_compile_definitions("NOMINMAX") 27 | 28 | # From OpenUSD/cmake/defaults/msvcdefaults.cmake 29 | # 30 | # The /Zc:inline option strips out the "arch_ctor_" symbols used for 31 | # library initialization by ARCH_CONSTRUCTOR starting in Visual Studio 2019, 32 | # causing release builds to fail. Disable the option for this and later 33 | # versions. 34 | # 35 | # For more details, see: 36 | # https://developercommunity.visualstudio.com/content/problem/914943/zcinline-removes-extern-symbols-inside-anonymous-n.html 37 | if (MSVC_VERSION GREATER_EQUAL 1920) 38 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zc:inline-") 39 | else() 40 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zc:inline") 41 | endif() 42 | endif() 43 | add_compile_definitions("BOOST_NO_CXX98_FUNCTION_BASE") 44 | 45 | option(BUILD_TESTS "Build tests" ON) 46 | option(BUILD_DOCS "Build documentation" ON) 47 | option(BUILD_PYTHON_BINDINGS "Build Python Bindings" ON) 48 | option(BUNDLE_PYTHON_TESTS "Bundle Python tests per group (faster)" OFF) 49 | option(BUILD_SHARED_LIBS "Build Shared Library" ON) 50 | 51 | # Update build type from environment for CMake < 3.22 52 | if (DEFINED ENV{CMAKE_BUILD_TYPE}) 53 | set(CMAKE_BUILD_TYPE $ENV{CMAKE_BUILD_TYPE} 54 | CACHE STRING "Specifies the build type" FORCE) 55 | endif() 56 | 57 | # Make module finder scripts available for TBB and USD. 58 | set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/modules") 59 | 60 | # Silence "Up-to-date:" install messages 61 | set(CMAKE_INSTALL_MESSAGE NEVER) 62 | 63 | # Generate "compile_commands.json" for use by editors and other developer tools. 64 | # https://cmake.org/cmake/help/v3.5/variable/CMAKE_EXPORT_COMPILE_COMMANDS.html 65 | set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE) 66 | 67 | # Discover Python per best matching location instead of highest version. 68 | # https://cmake.org/cmake/help/latest/policy/CMP0094.html 69 | cmake_policy(SET CMP0094 NEW) 70 | 71 | find_package(USD 0.20.11 REQUIRED) 72 | find_package(TBB 2017.0 COMPONENTS tbb REQUIRED) 73 | 74 | if(BUILD_PYTHON_BINDINGS) 75 | # The 'manylinux' images do not include the Python library. 76 | # CMake >= 3.18 is required for this option to work as expected. 77 | # https://github.com/pypa/manylinux 78 | if (TARGET_PYTHON_MODULE OR ($ENV{TARGET_PYTHON_MODULE})) 79 | find_package(Python COMPONENTS Interpreter Development.Module REQUIRED) 80 | add_library(Python::Python ALIAS Python::Module) 81 | else() 82 | find_package(Python COMPONENTS Interpreter Development REQUIRED) 83 | endif() 84 | 85 | # Convenient variable to fetch module against Python version found. 86 | set(_py_version ${Python_VERSION_MAJOR}${Python_VERSION_MINOR}) 87 | mark_as_advanced(_py_version) 88 | 89 | if (NOT USD_USE_INTERNAL_BOOST_PYTHON) 90 | find_package(Boost 1.70.0 COMPONENTS "python${_py_version}" REQUIRED) 91 | 92 | # Define generic target for Boost Python if necessary. 93 | if (NOT TARGET Boost::python) 94 | add_library(Boost::python ALIAS "Boost::python${_py_version}") 95 | endif() 96 | endif() 97 | 98 | # Set variable to identify the path to install and test python libraries. 99 | set(PYTHON_VERSION 100 | "${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}" 101 | CACHE INTERNAL "Python version.") 102 | 103 | set(PYTHON_DESTINATION 104 | "${CMAKE_INSTALL_LIBDIR}/python${PYTHON_VERSION}/site-packages" 105 | CACHE INTERNAL "Python library path.") 106 | endif() 107 | 108 | add_subdirectory(src) 109 | 110 | if (BUILD_TESTS) 111 | find_package(GTest 1.8.0 REQUIRED) 112 | include(GoogleTest) 113 | 114 | if(BUILD_PYTHON_BINDINGS) 115 | find_package(Pytest 4.6.11 REQUIRED) 116 | endif() 117 | 118 | enable_testing() 119 | 120 | add_subdirectory(test) 121 | endif() 122 | 123 | if (BUILD_DOCS) 124 | find_package(Sphinx 1.8.6 REQUIRED) 125 | find_package(Doxygen 1.8.5 REQUIRED) 126 | 127 | add_subdirectory(doc) 128 | endif() 129 | 130 | # Add format target if clang-format is found. 131 | find_package(ClangFormat 7) 132 | 133 | include(CMakePackageConfigHelpers) 134 | 135 | configure_package_config_file( 136 | "cmake/unf-config.cmake.in" 137 | "${CMAKE_CURRENT_BINARY_DIR}/unf-config.cmake" 138 | INSTALL_DESTINATION share/cmake/unf 139 | ) 140 | 141 | write_basic_package_version_file( 142 | "unf-config-version.cmake" 143 | COMPATIBILITY AnyNewerVersion 144 | ) 145 | 146 | install( 147 | FILES 148 | "${CMAKE_CURRENT_BINARY_DIR}/unf-config.cmake" 149 | "${CMAKE_CURRENT_BINARY_DIR}/unf-config-version.cmake" 150 | DESTINATION share/cmake/unf 151 | ) 152 | -------------------------------------------------------------------------------- /doc/sphinx/installing.rst: -------------------------------------------------------------------------------- 1 | .. _installing: 2 | 3 | ********** 4 | Installing 5 | ********** 6 | 7 | .. highlight:: bash 8 | 9 | The library can be installed using :term:`CMake` (any version over `3.15 10 | `_). 11 | 12 | .. _installing/dependencies: 13 | 14 | Dependencies 15 | ============ 16 | 17 | Ensure that a minimal installation of :term:`USD` is available. Headers and 18 | compiled libraries from :term:`USD` as well as headers from transitive 19 | dependencies such as :term:`TBB` are required during the building process. 20 | 21 | If Python bindings are needed, :term:`USD` must be built with Python support. 22 | Compiled libraries and headers from Python are also required during the 23 | building process. 24 | 25 | .. seealso:: 26 | 27 | `Building USD 28 | `_ 29 | 30 | 31 | Custom search paths to dependent packages can be provided with the following 32 | :term:`CMake` options (or environment variable): 33 | 34 | ============================= ========================================================= 35 | Option / Environment Variable Description 36 | ============================= ========================================================= 37 | USD_ROOT Add search path to :term:`USD` package. 38 | TBB_ROOT Add search path to :term:`TBB` package. 39 | Python_ROOT Add search path to Python package. 40 | Pytest_ROOT Add search path to :term:`pytest` program. 41 | Doxygen_ROOT Add search path to :term:`doxygen` program. 42 | Sphinx_ROOT Add search path to :term:`sphinx-build ` program. 43 | ClangFormat_ROOT Add search path to :term:`clang-format` program. 44 | ============================= ========================================================= 45 | 46 | .. note:: 47 | 48 | These feature is provided by :term:`CMake` under the `CMP0074 49 | `_ policy 50 | 51 | .. _installing/building: 52 | 53 | Building library 54 | ================ 55 | 56 | Obtain a copy of the source by either downloading the 57 | `zipball `_ or 58 | cloning the public repository:: 59 | 60 | git clone git@github.com:wdas/unf.git 61 | 62 | Then you can build and install the library as follows:: 63 | 64 | cd unf 65 | mkdir build && cd build 66 | cmake -DCMAKE_INSTALL_PREFIX=/path/to/destination .. 67 | cmake --build . --target install 68 | 69 | Here are a few :term:`CMake` options that can be used to influence the building 70 | process: 71 | 72 | ===================== ================================================================== 73 | Option Description 74 | ===================== ================================================================== 75 | BUILD_TESTS Indicate whether tests should be built. Default is true. 76 | BUILD_DOCS Indicate whether documentation should be built. Default is true. 77 | BUILD_PYTHON_BINDINGS Indicate whether Python bindings should be built. Default is true. 78 | BUILD_SHARED_LIBS Indicate whether library should be built shared. Default is true. 79 | BUNDLE_PYTHON_TESTS Bundle Python tests per group (faster). Default is false. 80 | ===================== ================================================================== 81 | 82 | The library can then be used by other programs or libraries via the ``unf::unf`` 83 | :term:`Cmake` target. 84 | 85 | .. _installing/clang-format: 86 | 87 | Apply clang-format 88 | ================== 89 | 90 | Ensure that :term:`Clang-format` is installed for applying C++ style. 91 | 92 | Then run the program as follows:: 93 | 94 | cmake --build . --target format 95 | 96 | .. warning:: 97 | 98 | When contributing, please run this command before committing your code. 99 | 100 | .. _installing/documentation: 101 | 102 | Building documentation 103 | ====================== 104 | 105 | Ensure that :term:`Doxygen` is installed. The required Python dependencies 106 | must also be installed as follows:: 107 | 108 | pip install -r doc/requirements.txt 109 | 110 | Then build the documentation as follows:: 111 | 112 | cmake --build . --target documentation 113 | 114 | .. note:: 115 | 116 | Documentation is automatically built with default installation, unless you 117 | set the ``BUILD_DOCS`` :term:`CMake` option to false. 118 | 119 | .. _installing/test: 120 | 121 | Running tests 122 | ============= 123 | 124 | Ensure that :term:`GTest` is installed. The required Python dependencies 125 | must also be installed as follows:: 126 | 127 | pip install -r test/requirements.txt 128 | 129 | .. note:: 130 | 131 | Python dependencies are not necessary if the ``BUILD_PYTHON_BINDINGS`` 132 | :term:`CMake` option is set to false. 133 | 134 | Once the library and all tests are built, you can run the tests using 135 | :term:`Ctest` within the build folder as follows:: 136 | 137 | ctest 138 | 139 | You can increase the verbosity and filter in one or several tests as follows:: 140 | 141 | ctest -VV 142 | ctest -R BrokerTest.Create -VV 143 | ctest -R BrokerTest.* -VV 144 | 145 | .. note:: 146 | 147 | Tests are automatically built with default installation, unless you 148 | set the ``BUILD_TESTS`` :term:`CMake` option to false. 149 | 150 | By default, unit tests and integration tests in Python will be decomposed into 151 | separated tests that can be individually filtered. Set the 152 | ``BUNDLE_PYTHON_TESTS`` :term:`CMake` option (or environment variable) to true 153 | if you want to combine Python tests per test type. 154 | -------------------------------------------------------------------------------- /src/unf/dispatcher.h: -------------------------------------------------------------------------------- 1 | #ifndef USD_NOTICE_FRAMEWORK_DISPATCHER_H 2 | #define USD_NOTICE_FRAMEWORK_DISPATCHER_H 3 | 4 | /// \file unf/dispatcher.h 5 | 6 | #include "unf/broker.h" 7 | #include "unf/notice.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace unf { 17 | 18 | /// \class Dispatcher 19 | /// 20 | /// \brief 21 | /// Interface for objects emitting standalone notices triggered by incoming 22 | /// PXR_NS::TfNotice derived notices. 23 | class Dispatcher : public PXR_NS::TfRefBase, public PXR_NS::TfWeakBase { 24 | public: 25 | /// Revoke all registered listeners on destruction. 26 | UNF_API virtual ~Dispatcher() { Revoke(); }; 27 | 28 | /// \brief 29 | /// Get unique string identifier 30 | /// 31 | /// Identifier will be used to retrieve Dispatcher from the Broker. 32 | /// 33 | /// \sa 34 | /// Broker::GetDispatcher 35 | UNF_API virtual std::string GetIdentifier() const = 0; 36 | 37 | /// Register listeners to PXR_NS::TfNotice derived notices. 38 | UNF_API virtual void Register() = 0; 39 | 40 | /// Revoke all registered listeners. 41 | UNF_API virtual void Revoke(); 42 | 43 | protected: 44 | /// Create a dispatcher targeting a Broker. 45 | UNF_API Dispatcher(const BrokerWeakPtr&); 46 | 47 | /// \brief 48 | /// Convenient templated method to register a listener for incoming 49 | /// \p InputNotice notice which emits a \p OutputNotice notice. 50 | /// 51 | /// The following example will emit a UnfNotice::ObjectsChanged notice for 52 | /// each PXR_NS::UsdNotice::ObjectsChanged received. 53 | /// 54 | /// \code{.cpp} 55 | /// _Register< 56 | /// PXR_NS::UsdNotice::ObjectsChanged, 57 | /// UnfNotice::ObjectsChanged>(); 58 | /// \endcode 59 | /// 60 | /// \warning 61 | /// The \p OutputNotice notice must be derived from 62 | /// UnfNotice::StageNotice and must have a constructor which takes an 63 | /// instance of \p InputNotice. 64 | template 65 | void _Register() 66 | { 67 | auto self = PXR_NS::TfCreateWeakPtr(this); 68 | auto cb = &Dispatcher::_OnReceiving; 69 | _keys.push_back( 70 | PXR_NS::TfNotice::Register(self, cb, _broker->GetStage())); 71 | } 72 | 73 | /// \brief 74 | /// Convenient templated method to emit a \p OutputNotice notice from an 75 | /// incoming \p InputNotice notice. 76 | /// 77 | /// \warning 78 | /// The \p OutputNotice notice must be derived from 79 | /// UnfNotice::StageNotice and must have a constructor which takes an 80 | /// instance of \p InputNotice. 81 | template 82 | void _OnReceiving(const InputNotice& notice) 83 | { 84 | PXR_NS::TfRefPtr _notice = OutputNotice::Create(notice); 85 | _broker->Send(_notice); 86 | } 87 | 88 | /// Broker that the dispatcher is attached to. 89 | BrokerWeakPtr _broker; 90 | 91 | /// List of handle-objects used for registering listeners. 92 | std::vector _keys; 93 | }; 94 | 95 | /// \class StageDispatcher 96 | /// 97 | /// \brief 98 | /// Default dispatcher which emits UnfNotice::StageNotice derived notices 99 | /// corresponding to each PXR_NS::UsdNotice::StageNotice derived notice 100 | /// received. 101 | class StageDispatcher : public Dispatcher { 102 | public: 103 | virtual std::string GetIdentifier() const override 104 | { 105 | return "StageDispatcher"; 106 | } 107 | 108 | /// \brief 109 | /// Register listeners to each PXR_NS::UsdNotice::StageNotice derived 110 | /// notices. 111 | virtual void Register() override; 112 | 113 | private: 114 | StageDispatcher(const BrokerWeakPtr& broker); 115 | 116 | /// Only a Broker can create a StageDispatcher. 117 | friend class Broker; 118 | }; 119 | 120 | /// \class DispatcherFactory 121 | /// 122 | /// \brief 123 | /// Interface for building Dispatcher type. 124 | /// 125 | /// \sa 126 | /// DispatcherFactoryImpl 127 | class DispatcherFactory : public PXR_NS::TfType::FactoryBase { 128 | public: 129 | /// Base constructor to create a Dispatcher. 130 | virtual PXR_NS::TfRefPtr New( 131 | const BrokerWeakPtr& broker) const = 0; 132 | }; 133 | 134 | /// \class DispatcherFactoryImpl 135 | /// 136 | /// \brief 137 | /// Templated factory class which creates a specific type of Dispatcher. 138 | /// 139 | /// \sa 140 | /// DispatcherDefine 141 | template 142 | class DispatcherFactoryImpl : public DispatcherFactory { 143 | public: 144 | /// Create a Dispatcher and return reference pointer. 145 | virtual PXR_NS::TfRefPtr New( 146 | const BrokerWeakPtr& broker) const override 147 | { 148 | return PXR_NS::TfCreateRefPtr(new T(broker)); 149 | } 150 | }; 151 | 152 | /// \fn DispatcherDefine 153 | /// 154 | /// \brief 155 | /// Define a PXR_NS::TfType for a specific type of Dispatcher. 156 | /// 157 | /// Typical usage to define a type for a dispatcher \p Foo would be: 158 | /// 159 | /// \code{.cpp} 160 | /// TF_REGISTRY_FUNCTION(PXR_NS::TfType) 161 | /// { 162 | /// DispatcherDefine(); 163 | /// } 164 | /// \endcode 165 | template 166 | void DispatcherDefine() 167 | { 168 | PXR_NS::TfType::Define >() 169 | .template SetFactory >(); 170 | } 171 | 172 | } // namespace unf 173 | 174 | #endif // USD_NOTICE_FRAMEWORK_DISPATCHER_H 175 | -------------------------------------------------------------------------------- /test/unit/python/test_objects_changed.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pxr import Usd, Tf, Sdf 4 | import unf 5 | 6 | 7 | def test_objects_changed(): 8 | """Test whether ObjectsChanged notice is as expected.""" 9 | stage = Usd.Stage.CreateInMemory() 10 | unf.Broker.Create(stage) 11 | 12 | stage.DefinePrim("/Foo") 13 | 14 | received = [] 15 | 16 | def _validate(notice, stage): 17 | """Validate notice received.""" 18 | assert notice.IsMergeable() is True 19 | assert notice.GetTypeId() == "unf::UnfNotice::ObjectsChanged" 20 | received.append(notice) 21 | 22 | key = Tf.Notice.Register(unf.Notice.ObjectsChanged, _validate, stage) 23 | 24 | stage.DefinePrim("/Bar") 25 | 26 | # Ensure that one notice was received. 27 | assert len(received) == 1 28 | 29 | 30 | def test_objects_changed_resynced_object(): 31 | """Check whether object has been resynced.""" 32 | stage = Usd.Stage.CreateInMemory() 33 | unf.Broker.Create(stage) 34 | 35 | stage.DefinePrim("/Foo") 36 | 37 | received = [] 38 | 39 | def _validate(notice, stage): 40 | """Validate notice received.""" 41 | assert notice.ResyncedObject(stage.GetPrimAtPath("/Foo")) is False 42 | assert notice.ResyncedObject(stage.GetPrimAtPath("/Bar")) is True 43 | assert notice.AffectedObject(stage.GetPrimAtPath("/Foo")) is False 44 | assert notice.AffectedObject(stage.GetPrimAtPath("/Bar")) is True 45 | received.append(notice) 46 | 47 | key = Tf.Notice.Register(unf.Notice.ObjectsChanged, _validate, stage) 48 | 49 | stage.DefinePrim("/Bar") 50 | 51 | # Ensure that one notice was received. 52 | assert len(received) == 1 53 | 54 | 55 | def test_objects_changed_changed_info_only(): 56 | """Check whether only info from object has changed.""" 57 | stage = Usd.Stage.CreateInMemory() 58 | unf.Broker.Create(stage) 59 | 60 | stage.DefinePrim("/Foo") 61 | prim = stage.DefinePrim("/Bar") 62 | 63 | received = [] 64 | 65 | def _validate(notice, stage): 66 | """Validate notice received.""" 67 | assert notice.ChangedInfoOnly(stage.GetPrimAtPath("/Foo")) is False 68 | assert notice.ChangedInfoOnly(stage.GetPrimAtPath("/Bar")) is True 69 | assert notice.AffectedObject(stage.GetPrimAtPath("/Foo")) is False 70 | assert notice.AffectedObject(stage.GetPrimAtPath("/Bar")) is True 71 | received.append(notice) 72 | 73 | key = Tf.Notice.Register(unf.Notice.ObjectsChanged, _validate, stage) 74 | 75 | prim.SetMetadata("comment", "This is a test") 76 | 77 | # Ensure that one notice was received. 78 | assert len(received) == 1 79 | 80 | 81 | def test_objects_changed_get_resynced_paths(): 82 | """Ensure that expected resynced paths are returned.""" 83 | stage = Usd.Stage.CreateInMemory() 84 | unf.Broker.Create(stage) 85 | 86 | received = [] 87 | 88 | def _validate(notice, stage): 89 | """Validate notice received.""" 90 | assert notice.GetResyncedPaths() == [Sdf.Path("/Foo")] 91 | received.append(notice) 92 | 93 | key = Tf.Notice.Register(unf.Notice.ObjectsChanged, _validate, stage) 94 | 95 | stage.DefinePrim("/Foo") 96 | 97 | # Ensure that one notice was received. 98 | assert len(received) == 1 99 | 100 | 101 | def test_objects_changed_get_changed_info_only_paths(): 102 | """Ensure that expected paths with non-resyncable info are returned.""" 103 | stage = Usd.Stage.CreateInMemory() 104 | unf.Broker.Create(stage) 105 | 106 | prim = stage.DefinePrim("/Foo") 107 | 108 | received = [] 109 | 110 | def _validate(notice, stage): 111 | """Validate notice received.""" 112 | assert notice.GetChangedInfoOnlyPaths() == [Sdf.Path("/Foo")] 113 | received.append(notice) 114 | 115 | key = Tf.Notice.Register(unf.Notice.ObjectsChanged, _validate, stage) 116 | 117 | prim.SetMetadata("comment", "This is a test") 118 | 119 | # Ensure that one notice was received. 120 | assert len(received) == 1 121 | 122 | def test_objects_changed_get_changed_fields(): 123 | """Ensure that expected set of fields are returned.""" 124 | stage = Usd.Stage.CreateInMemory() 125 | unf.Broker.Create(stage) 126 | 127 | prim = stage.DefinePrim("/Foo") 128 | 129 | received = [] 130 | 131 | def _validate(notice, stage): 132 | """Validate notice received.""" 133 | assert notice.GetChangedFields(prim) == ["comment"] 134 | assert notice.GetChangedFields(Sdf.Path("/Foo")) == ["comment"] 135 | assert notice.GetChangedFields(Sdf.Path("/Incorrect")) == [] 136 | received.append(notice) 137 | 138 | key = Tf.Notice.Register(unf.Notice.ObjectsChanged, _validate, stage) 139 | 140 | prim.SetMetadata("comment", "This is a test") 141 | 142 | # Ensure that one notice was received. 143 | assert len(received) == 1 144 | 145 | def test_objects_changed_has_changed_fields(): 146 | """Check whether path or prim have changed fields.""" 147 | stage = Usd.Stage.CreateInMemory() 148 | unf.Broker.Create(stage) 149 | 150 | prim = stage.DefinePrim("/Foo") 151 | 152 | received = [] 153 | 154 | def _validate(notice, stage): 155 | """Validate notice received.""" 156 | assert notice.HasChangedFields(prim) is True 157 | assert notice.HasChangedFields(Sdf.Path("/Foo")) is True 158 | assert notice.HasChangedFields(Sdf.Path("/Incorrect")) is False 159 | received.append(notice) 160 | 161 | key = Tf.Notice.Register(unf.Notice.ObjectsChanged, _validate, stage) 162 | 163 | prim.SetMetadata("comment", "This is a test") 164 | 165 | # Ensure that one notice was received. 166 | assert len(received) == 1 167 | -------------------------------------------------------------------------------- /test/integration/python/test_add_prims.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pxr import Usd, Tf 4 | import unf 5 | 6 | import pytest 7 | 8 | 9 | @pytest.mark.parametrize("notice_type, excepted", [ 10 | ("StageNotice", 6), 11 | ("StageContentsChanged", 3), 12 | ("ObjectsChanged", 3), 13 | ("StageEditTargetChanged", 0), 14 | ("LayerMutingChanged", 0), 15 | ], ids=[ 16 | "StageNotice", 17 | "StageContentsChanged", 18 | "ObjectsChanged", 19 | "StageEditTargetChanged", 20 | "LayerMutingChanged", 21 | ]) 22 | def test_add_prims(notice_type, excepted): 23 | """Add several prims to the stage. 24 | """ 25 | stage = Usd.Stage.CreateInMemory() 26 | unf.Broker.Create(stage) 27 | 28 | # Listen to broker notice. 29 | received_broker = [] 30 | key1 = Tf.Notice.Register( 31 | getattr(unf.Notice, notice_type), 32 | lambda n, _: received_broker.append(n), stage) 33 | 34 | # Listen to corresponding USD notice. 35 | received_usd = [] 36 | key2 = Tf.Notice.Register( 37 | getattr(Usd.Notice, notice_type), 38 | lambda n, _: received_usd.append(n), stage) 39 | 40 | stage.DefinePrim("/Foo") 41 | stage.DefinePrim("/Bar") 42 | stage.DefinePrim("/Baz") 43 | 44 | # Ensure that we received the same number of notices. 45 | assert len(received_broker) == excepted 46 | assert len(received_usd) == excepted 47 | 48 | @pytest.mark.parametrize("notice_type, expected_usd, expected_broker", [ 49 | ("StageNotice", 6, 2), 50 | ("StageContentsChanged", 3, 1), 51 | ("ObjectsChanged", 3, 1), 52 | ("StageEditTargetChanged", 0, 0), 53 | ("LayerMutingChanged", 0, 0), 54 | ], ids=[ 55 | "StageNotice", 56 | "StageContentsChanged", 57 | "ObjectsChanged", 58 | "StageEditTargetChanged", 59 | "LayerMutingChanged", 60 | ]) 61 | def test_add_prims_batching(notice_type, expected_usd, expected_broker): 62 | """Add several prims to the stage and batch broker notices. 63 | """ 64 | stage = Usd.Stage.CreateInMemory() 65 | broker = unf.Broker.Create(stage) 66 | 67 | # Listen to broker notice. 68 | received_broker = [] 69 | key1 = Tf.Notice.Register( 70 | getattr(unf.Notice, notice_type), 71 | lambda n, _: received_broker.append(n), stage) 72 | 73 | # Listen to corresponding USD notice. 74 | received_usd = [] 75 | key2 = Tf.Notice.Register( 76 | getattr(Usd.Notice, notice_type), 77 | lambda n, _: received_usd.append(n), stage) 78 | 79 | broker.BeginTransaction() 80 | 81 | stage.DefinePrim("/Foo") 82 | stage.DefinePrim("/Bar") 83 | stage.DefinePrim("/Baz") 84 | 85 | # Ensure that broker notices are not sent during a transaction. 86 | assert len(received_broker) == 0 87 | 88 | # While USD Notices are being sent as expected. 89 | assert len(received_usd) == expected_usd 90 | 91 | broker.EndTransaction() 92 | 93 | # Ensure that consolidated broker notices are sent after a transaction. 94 | assert len(received_broker) == expected_broker 95 | 96 | @pytest.mark.parametrize("notice_type, expected_usd", [ 97 | ("StageNotice", 6), 98 | ("StageContentsChanged", 3), 99 | ("ObjectsChanged", 3), 100 | ("StageEditTargetChanged", 0), 101 | ("LayerMutingChanged", 0), 102 | ], ids=[ 103 | "StageNotice", 104 | "StageContentsChanged", 105 | "ObjectsChanged", 106 | "StageEditTargetChanged", 107 | "LayerMutingChanged", 108 | ]) 109 | 110 | def test_add_prims_blocking(notice_type, expected_usd): 111 | """Add several prims to the stage and block broker notices. 112 | """ 113 | stage = Usd.Stage.CreateInMemory() 114 | broker = unf.Broker.Create(stage) 115 | 116 | # Listen to broker notice. 117 | received_broker = [] 118 | key1 = Tf.Notice.Register( 119 | getattr(unf.Notice, notice_type), 120 | lambda n, _: received_broker.append(n), stage) 121 | 122 | # Listen to corresponding USD notice. 123 | received_usd = [] 124 | key2 = Tf.Notice.Register( 125 | getattr(Usd.Notice, notice_type), 126 | lambda n, _: received_usd.append(n), stage) 127 | 128 | # Predicate blocking all broker notices. 129 | broker.BeginTransaction(predicate=lambda _: False) 130 | 131 | stage.DefinePrim("/Foo") 132 | stage.DefinePrim("/Bar") 133 | stage.DefinePrim("/Baz") 134 | 135 | # Ensure that broker notices are not sent during a transaction. 136 | assert len(received_broker) == 0 137 | 138 | # While USD Notices are being sent as expected. 139 | assert len(received_usd) == expected_usd 140 | 141 | broker.EndTransaction() 142 | 143 | # Ensure that no broker notices have been received. 144 | assert len(received_broker) == 0 145 | 146 | def test_add_prims_transaction_objectschanged(): 147 | """Add several prims to the stage during transaction and analyze 148 | ObjectsChanged notice. 149 | 150 | """ 151 | stage = Usd.Stage.CreateInMemory() 152 | broker = unf.Broker.Create(stage) 153 | 154 | received = [] 155 | 156 | def _validate(notice, stage): 157 | """Validate notice received.""" 158 | assert len(notice.GetResyncedPaths()) == 3 159 | assert len(notice.GetChangedInfoOnlyPaths()) == 0 160 | assert notice.GetResyncedPaths()[0] == "/Bar" 161 | assert notice.GetResyncedPaths()[1] == "/Baz" 162 | assert notice.GetResyncedPaths()[2] == "/Foo" 163 | received.append(notice) 164 | 165 | key = Tf.Notice.Register(unf.Notice.ObjectsChanged, _validate, stage) 166 | 167 | broker.BeginTransaction() 168 | 169 | stage.DefinePrim("/Foo") 170 | stage.DefinePrim("/Bar") 171 | stage.DefinePrim("/Baz") 172 | 173 | broker.EndTransaction() 174 | 175 | # Ensure that one notice was received. 176 | assert len(received) == 1 177 | -------------------------------------------------------------------------------- /.github/workflows/test-windows.yml: -------------------------------------------------------------------------------- 1 | name: test-windows 2 | 3 | on: 4 | push: 5 | branches: [ main, dev ] 6 | pull_request: 7 | branches: [ main, dev ] 8 | 9 | # Run tests once a week on Sunday. 10 | schedule: 11 | - cron: "0 6 * * 0" 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | test-windows: 18 | runs-on: windows-2022 19 | timeout-minutes: 300 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | usd: ["v24.08", "v25.05.01"] 25 | python: ["", "3.10", "3.12"] 26 | 27 | name: "USD-${{ matrix.usd }}${{ matrix.python != '' && format('-py{0}', matrix.python) || '-no-py' }}" 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | - name: Set up Python ${{ matrix.python }} 33 | if: matrix.python != '' 34 | uses: actions/setup-python@v5 35 | with: 36 | python-version: ${{ matrix.python }} 37 | 38 | - name: Create Build Environment 39 | run: | 40 | vcpkg install --triplet=x64-windows gtest 41 | cmake -E make_directory ${{github.workspace}}\build 42 | cmake -E make_directory ${{runner.temp}}\USD 43 | 44 | - name: Install test Python deps 45 | if: matrix.python != '' 46 | run: | 47 | python -m pip install -r ${{github.workspace}}/test/requirements.txt 48 | 49 | - name: Download USD 50 | working-directory: ${{runner.temp}}/USD 51 | shell: cmd 52 | run: | 53 | git clone https://github.com/PixarAnimationStudios/OpenUSD.git ^ 54 | --depth 1 --branch ${{ matrix.usd }} ./src 55 | 56 | - name: Apply patch for USD v24.08 57 | if: matrix.usd == 'v24.08' 58 | working-directory: ${{runner.temp}}/USD 59 | shell: bash 60 | run: | 61 | sed -i '/BOOST_URL/ s|boostorg.jfrog.io.*/release|sourceforge.net/projects/boost/files/boost|' src/build_scripts/build_usd.py 62 | sed -i '/BOOST_URL/ s|source/boost|boost|' src/build_scripts/build_usd.py 63 | sed -i '/BOOST_URL/ s|\.zip"|.zip/download"|' src/build_scripts/build_usd.py 64 | 65 | - name: Install USD 66 | if: matrix.python != '' 67 | working-directory: ${{runner.temp}}/USD 68 | shell: cmd 69 | run: | 70 | call "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat" 71 | set VCPKG_TARGET_TRIPLET=x64-windows 72 | set CMAKE_TOOLCHAIN_FILE=%VCPKG_INSTALLATION_ROOT%/scripts/buildsystems/vcpkg.cmake 73 | python ./src/build_scripts/build_usd.py . ^ 74 | --generator "Visual Studio 17 2022" ^ 75 | --no-tests ^ 76 | --no-examples ^ 77 | --no-tutorials ^ 78 | --no-tools ^ 79 | --no-docs ^ 80 | --no-imaging ^ 81 | --no-materialx ^ 82 | -v 83 | 84 | - name: Install USD (without Python) 85 | if: matrix.python == '' 86 | working-directory: ${{runner.temp}}/USD 87 | shell: cmd 88 | run: | 89 | call "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat" 90 | set VCPKG_TARGET_TRIPLET=x64-windows 91 | set CMAKE_TOOLCHAIN_FILE=%VCPKG_INSTALLATION_ROOT%/scripts/buildsystems/vcpkg.cmake 92 | python ./src/build_scripts/build_usd.py . ^ 93 | --generator "Visual Studio 17 2022" ^ 94 | --no-tests ^ 95 | --no-examples ^ 96 | --no-tutorials ^ 97 | --no-tools ^ 98 | --no-docs ^ 99 | --no-imaging ^ 100 | --no-materialx ^ 101 | --no-python ^ 102 | -v 103 | 104 | - name: Configure & Build 105 | if: matrix.python != '' 106 | shell: bash 107 | working-directory: ${{github.workspace}}/build 108 | run: | 109 | export PATH="${{runner.temp}}/USD/bin;${{runner.temp}}/USD/lib;${PATH}" 110 | export PYTHONPATH="${{runner.temp}}/USD/lib/python;${PYTHONPATH}" 111 | cmake \ 112 | -G "Visual Studio 17 2022" -A x64 \ 113 | -D "CMAKE_TOOLCHAIN_FILE=${VCPKG_INSTALLATION_ROOT}/scripts/buildsystems/vcpkg.cmake" \ 114 | -D "VCPKG_TARGET_TRIPLET=x64-windows" \ 115 | -D "BUILD_DOCS=OFF" \ 116 | -D "CMAKE_INCLUDE_PATH=${{runner.temp}}/USD/include" \ 117 | -D "CMAKE_LIBRARY_PATH=${{runner.temp}}/USD/lib" \ 118 | .. 119 | cmake --build . --config Release 120 | 121 | - name: Configure & Build (without Python) 122 | if: matrix.python == '' 123 | shell: bash 124 | working-directory: ${{github.workspace}}/build 125 | run: | 126 | export PATH="${{runner.temp}}/USD/bin;${{runner.temp}}/USD/lib;${PATH}" 127 | cmake \ 128 | -G "Visual Studio 17 2022" -A x64 \ 129 | -D "CMAKE_TOOLCHAIN_FILE=${VCPKG_INSTALLATION_ROOT}/scripts/buildsystems/vcpkg.cmake" \ 130 | -D "VCPKG_TARGET_TRIPLET=x64-windows" \ 131 | -D "BUILD_DOCS=OFF" \ 132 | -D "BUILD_PYTHON_BINDINGS=OFF" \ 133 | -D "CMAKE_INCLUDE_PATH=${{runner.temp}}/USD/include" \ 134 | -D "CMAKE_LIBRARY_PATH=${{runner.temp}}/USD/lib" \ 135 | .. 136 | cmake --build . --config Release 137 | 138 | - name: Check for formatting errors 139 | shell: bash 140 | working-directory: ${{github.workspace}}/build 141 | run: | 142 | cmake --build . --target format 143 | STATUS_OUTPUT=$(git -C .. status --porcelain) 144 | if [ -n "$STATUS_OUTPUT" ]; then 145 | echo "Code formatting errors found:" 146 | git -C .. diff 147 | exit 1 148 | else 149 | echo "No formatting errors found." 150 | fi 151 | 152 | - name: Run Test 153 | shell: bash 154 | working-directory: ${{github.workspace}}/build 155 | run: ctest -VV -C Release 156 | env: 157 | CTEST_OUTPUT_ON_FAILURE: True 158 | -------------------------------------------------------------------------------- /src/unf/broker.cpp: -------------------------------------------------------------------------------- 1 | #include "unf/broker.h" 2 | #include "unf/capturePredicate.h" 3 | #include "unf/dispatcher.h" 4 | #include "unf/notice.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | PXR_NAMESPACE_USING_DIRECTIVE 12 | 13 | namespace unf { 14 | 15 | // Initiate static registry. 16 | std::unordered_map 17 | Broker::Registry; 18 | 19 | Broker::Broker(const UsdStageWeakPtr& stage) : _stage(stage) 20 | { 21 | // Add default dispatcher. 22 | _AddDispatcher(); 23 | 24 | // Discover dispatchers added via plugin to complete or override 25 | // default dispatcher. 26 | _DiscoverDispatchers(); 27 | 28 | // Register all dispatchers 29 | for (auto& element : _dispatcherMap) { 30 | element.second->Register(); 31 | } 32 | } 33 | 34 | BrokerPtr Broker::Create(const UsdStageWeakPtr& stage) 35 | { 36 | Broker::_CleanCache(); 37 | 38 | // If there doesn't exist a broker for the given stage, create a new broker. 39 | if (Registry.find(stage) == Registry.end()) { 40 | Registry[stage] = TfCreateRefPtr(new Broker(stage)); 41 | } 42 | 43 | return Registry[stage]; 44 | } 45 | 46 | const UsdStageWeakPtr Broker::GetStage() const { return _stage; } 47 | 48 | bool Broker::IsInTransaction() { return _mergers.size() > 0; } 49 | 50 | void Broker::BeginTransaction(CapturePredicate predicate) 51 | { 52 | _mergers.push_back(_NoticeMerger(predicate)); 53 | } 54 | 55 | void Broker::BeginTransaction(const CapturePredicateFunc& function) 56 | { 57 | _mergers.push_back(_NoticeMerger(CapturePredicate(function))); 58 | } 59 | 60 | void Broker::EndTransaction() 61 | { 62 | if (!IsInTransaction()) { 63 | return; 64 | } 65 | 66 | _NoticeMerger& merger = _mergers.back(); 67 | 68 | // If there are only one merger left, process all notices. 69 | if (_mergers.size() == 1) { 70 | merger.Merge(); 71 | merger.PostProcess(); 72 | merger.Send(_stage); 73 | } 74 | // Otherwise, it means that we are in a nested transaction that should 75 | // not be processed yet. Join data with next merger. 76 | else { 77 | (_mergers.end() - 2)->Join(merger); 78 | } 79 | 80 | _mergers.pop_back(); 81 | } 82 | 83 | void Broker::Send(const UnfNotice::StageNoticeRefPtr& notice) 84 | { 85 | if (_mergers.size() > 0) { 86 | _mergers.back().Add(notice); 87 | } 88 | // Otherwise, send the notice. 89 | else { 90 | notice->Send(_stage); 91 | } 92 | } 93 | 94 | DispatcherPtr& Broker::GetDispatcher(std::string identifier) 95 | { 96 | return _dispatcherMap.at(identifier); 97 | } 98 | 99 | void Broker::Reset() { Registry.erase(_stage); } 100 | 101 | void Broker::ResetAll() { Registry.clear(); } 102 | 103 | void Broker::_CleanCache() 104 | { 105 | for (auto it = Registry.begin(); it != Registry.end();) { 106 | // If the stage doesn't exist anymore, delete the corresponding 107 | // broker from the registry. 108 | if (it->second->GetStage().IsExpired()) { 109 | it = Registry.erase(it); 110 | } 111 | else { 112 | it++; 113 | } 114 | } 115 | } 116 | 117 | void Broker::_DiscoverDispatchers() 118 | { 119 | TfType root = TfType::Find(); 120 | std::set types; 121 | PlugRegistry::GetAllDerivedTypes(root, &types); 122 | 123 | for (const TfType& type : types) { 124 | _LoadFromPlugins(type); 125 | } 126 | } 127 | 128 | void Broker::_Add(const DispatcherPtr& dispatcher) 129 | { 130 | _dispatcherMap[dispatcher->GetIdentifier()] = dispatcher; 131 | } 132 | 133 | Broker::_NoticeMerger::_NoticeMerger(CapturePredicate predicate) 134 | : _predicate(std::move(predicate)) 135 | { 136 | } 137 | 138 | void Broker::_NoticeMerger::Add(const UnfNotice::StageNoticeRefPtr& notice) 139 | { 140 | // Indicate whether the notice needs to be captured. 141 | if (!_predicate(*notice)) return; 142 | 143 | // Store notices per type name, so that each type can be merged if 144 | // required. 145 | std::string name = notice->GetTypeId(); 146 | _noticeMap[name].push_back(notice); 147 | } 148 | 149 | void Broker::_NoticeMerger::Join(_NoticeMerger& merger) 150 | { 151 | for (auto& element : merger._noticeMap) { 152 | auto& source = element.second; 153 | auto& target = _noticeMap[element.first]; 154 | 155 | target.reserve(target.size() + source.size()); 156 | std::move( 157 | std::begin(source), std::end(source), std::back_inserter(target)); 158 | 159 | source.clear(); 160 | } 161 | 162 | merger._noticeMap.clear(); 163 | } 164 | 165 | void Broker::_NoticeMerger::Merge() 166 | { 167 | for (auto& element : _noticeMap) { 168 | auto& notices = element.second; 169 | 170 | // If there are more than one notice for this type and 171 | // if the notices are mergeable, we only need to keep the 172 | // first notice, and all other can be pruned. 173 | if (notices.size() > 1 && notices[0]->IsMergeable()) { 174 | auto& notice = notices.at(0); 175 | 176 | auto it = std::next(notices.begin()); 177 | for (; it != notices.end(); ++it) { 178 | // Attempt to merge content of notice with first notice 179 | // if this is possible. 180 | notice->Merge(std::move(**it)); 181 | } 182 | notices.resize(1); 183 | } 184 | } 185 | } 186 | 187 | void Broker::_NoticeMerger::PostProcess() 188 | { 189 | for (auto& element : _noticeMap) { 190 | auto& notice = element.second[0]; 191 | notice->PostProcess(); 192 | } 193 | } 194 | 195 | void Broker::_NoticeMerger::Send(const UsdStageWeakPtr& stage) 196 | { 197 | for (auto& element : _noticeMap) { 198 | auto& notices = element.second; 199 | 200 | // Send all remaining notices. 201 | for (const auto& notice : element.second) { 202 | notice->Send(stage); 203 | } 204 | } 205 | } 206 | 207 | } // namespace unf 208 | -------------------------------------------------------------------------------- /test/integration/testChangeEditTarget.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | 15 | // namespace aliases for convenience. 16 | using _USD = PXR_NS::UsdNotice; 17 | namespace _UNF = unf::UnfNotice; 18 | 19 | class ChangeEditTargetTest : public ::testing::Test { 20 | protected: 21 | using UsdListener = ::Test::Listener< 22 | _USD::StageNotice, _USD::StageContentsChanged, _USD::ObjectsChanged, 23 | _USD::StageEditTargetChanged, _USD::LayerMutingChanged>; 24 | 25 | using BrokerListener = ::Test::Listener< 26 | _UNF::StageNotice, _UNF::StageContentsChanged, _UNF::ObjectsChanged, 27 | _UNF::StageEditTargetChanged, _UNF::LayerMutingChanged>; 28 | 29 | void SetUp() override 30 | { 31 | _stage = PXR_NS::UsdStage::CreateInMemory(); 32 | _AddLayers(2); 33 | 34 | _usdListener.SetStage(_stage); 35 | _brokerListener.SetStage(_stage); 36 | } 37 | 38 | void _AddLayers(int number) 39 | { 40 | _layers.reserve(number); 41 | _layerIds.reserve(number); 42 | 43 | while (_layers.size() < number) { 44 | auto layer = PXR_NS::SdfLayer::CreateAnonymous(".usda"); 45 | _layers.push_back(layer); 46 | _layerIds.push_back(layer->GetIdentifier()); 47 | } 48 | 49 | _stage->GetRootLayer()->SetSubLayerPaths(_layerIds); 50 | } 51 | 52 | PXR_NS::UsdStageRefPtr _stage; 53 | std::vector _layers; 54 | std::vector _layerIds; 55 | 56 | UsdListener _usdListener; 57 | BrokerListener _brokerListener; 58 | }; 59 | 60 | TEST_F(ChangeEditTargetTest, Simple) 61 | { 62 | auto broker = unf::Broker::Create(_stage); 63 | 64 | _stage->SetEditTarget(PXR_NS::UsdEditTarget(_layers[0])); 65 | _stage->SetEditTarget(PXR_NS::UsdEditTarget(_layers[1])); 66 | 67 | // Ensure that similar notices are received via the stage and the broker. 68 | ASSERT_EQ(_usdListener.Received<_USD::StageNotice>(), 2); 69 | ASSERT_EQ(_usdListener.Received<_USD::StageContentsChanged>(), 0); 70 | ASSERT_EQ(_usdListener.Received<_USD::ObjectsChanged>(), 0); 71 | ASSERT_EQ(_usdListener.Received<_USD::StageEditTargetChanged>(), 2); 72 | ASSERT_EQ(_usdListener.Received<_USD::LayerMutingChanged>(), 0); 73 | 74 | ASSERT_EQ(_brokerListener.Received<_UNF::StageNotice>(), 2); 75 | ASSERT_EQ(_brokerListener.Received<_UNF::StageContentsChanged>(), 0); 76 | ASSERT_EQ(_brokerListener.Received<_UNF::ObjectsChanged>(), 0); 77 | ASSERT_EQ(_brokerListener.Received<_UNF::StageEditTargetChanged>(), 2); 78 | ASSERT_EQ(_brokerListener.Received<_UNF::LayerMutingChanged>(), 0); 79 | } 80 | 81 | TEST_F(ChangeEditTargetTest, Batching) 82 | { 83 | auto broker = unf::Broker::Create(_stage); 84 | 85 | broker->BeginTransaction(); 86 | 87 | _stage->SetEditTarget(PXR_NS::UsdEditTarget(_layers[0])); 88 | _stage->SetEditTarget(PXR_NS::UsdEditTarget(_layers[1])); 89 | 90 | // Ensure that USD Notices are being sent as expected. 91 | ASSERT_EQ(_usdListener.Received<_USD::StageNotice>(), 2); 92 | ASSERT_EQ(_usdListener.Received<_USD::StageContentsChanged>(), 0); 93 | ASSERT_EQ(_usdListener.Received<_USD::ObjectsChanged>(), 0); 94 | ASSERT_EQ(_usdListener.Received<_USD::StageEditTargetChanged>(), 2); 95 | ASSERT_EQ(_usdListener.Received<_USD::LayerMutingChanged>(), 0); 96 | 97 | // While broker notices are blocked during a transaction. 98 | ASSERT_EQ(_brokerListener.Received<_UNF::StageNotice>(), 0); 99 | ASSERT_EQ(_brokerListener.Received<_UNF::StageContentsChanged>(), 0); 100 | ASSERT_EQ(_brokerListener.Received<_UNF::ObjectsChanged>(), 0); 101 | ASSERT_EQ(_brokerListener.Received<_UNF::StageEditTargetChanged>(), 0); 102 | ASSERT_EQ(_brokerListener.Received<_UNF::LayerMutingChanged>(), 0); 103 | 104 | broker->EndTransaction(); 105 | 106 | // Ensure that consolidated broker notices are sent after a transaction. 107 | ASSERT_EQ(_brokerListener.Received<_UNF::StageNotice>(), 1); 108 | ASSERT_EQ(_brokerListener.Received<_UNF::StageContentsChanged>(), 0); 109 | ASSERT_EQ(_brokerListener.Received<_UNF::ObjectsChanged>(), 0); 110 | ASSERT_EQ(_brokerListener.Received<_UNF::StageEditTargetChanged>(), 1); 111 | ASSERT_EQ(_brokerListener.Received<_UNF::LayerMutingChanged>(), 0); 112 | } 113 | 114 | TEST_F(ChangeEditTargetTest, Blocking) 115 | { 116 | auto broker = unf::Broker::Create(_stage); 117 | 118 | // Pass a predicate to block all broker notices. 119 | broker->BeginTransaction([](const _UNF::StageNotice &) { return false; }); 120 | 121 | _stage->SetEditTarget(PXR_NS::UsdEditTarget(_layers[0])); 122 | _stage->SetEditTarget(PXR_NS::UsdEditTarget(_layers[1])); 123 | 124 | // Ensure that USD Notices are being sent as expected. 125 | ASSERT_EQ(_usdListener.Received<_USD::StageNotice>(), 2); 126 | ASSERT_EQ(_usdListener.Received<_USD::StageContentsChanged>(), 0); 127 | ASSERT_EQ(_usdListener.Received<_USD::ObjectsChanged>(), 0); 128 | ASSERT_EQ(_usdListener.Received<_USD::StageEditTargetChanged>(), 2); 129 | ASSERT_EQ(_usdListener.Received<_USD::LayerMutingChanged>(), 0); 130 | 131 | // While broker notices are blocked during a transaction. 132 | ASSERT_EQ(_brokerListener.Received<_UNF::StageNotice>(), 0); 133 | ASSERT_EQ(_brokerListener.Received<_UNF::StageContentsChanged>(), 0); 134 | ASSERT_EQ(_brokerListener.Received<_UNF::ObjectsChanged>(), 0); 135 | ASSERT_EQ(_brokerListener.Received<_UNF::StageEditTargetChanged>(), 0); 136 | ASSERT_EQ(_brokerListener.Received<_UNF::LayerMutingChanged>(), 0); 137 | 138 | broker->EndTransaction(); 139 | 140 | // Ensure that no broker notices are sent after a transaction either. 141 | ASSERT_EQ(_brokerListener.Received<_UNF::StageNotice>(), 0); 142 | ASSERT_EQ(_brokerListener.Received<_UNF::StageContentsChanged>(), 0); 143 | ASSERT_EQ(_brokerListener.Received<_UNF::ObjectsChanged>(), 0); 144 | ASSERT_EQ(_brokerListener.Received<_UNF::StageEditTargetChanged>(), 0); 145 | ASSERT_EQ(_brokerListener.Received<_UNF::LayerMutingChanged>(), 0); 146 | } 147 | -------------------------------------------------------------------------------- /doc/doxygen/USD.tag: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PXR_NS 5 | 6 | PXR_NS::TfNotice 7 | PXR_NS::TfType 8 | PXR_NS::TfRefBase 9 | PXR_NS::TfRefPtr 10 | PXR_NS::TfWeakBase 11 | PXR_NS::TfWeakPtr 12 | PXR_NS::TfAnyWeakPtr 13 | PXR_NS::TfToken 14 | PXR_NS::SdfPath 15 | PXR_NS::UsdObject 16 | PXR_NS::UsdStage 17 | PXR_NS::UsdStageRefPtr 18 | PXR_NS::UsdStageWeakPtr 19 | PXR_NS::UsdNotice 20 | 21 | 22 | PXR_NS::TfNotice 23 | class_tf_notice.html 24 | PXR_NS::TfType::FactoryBase 25 | 26 | 27 | PXR_NS::TfNotice::Key 28 | class_tf_notice_1_1_key.html 29 | 30 | 31 | PXR_NS::TfType 32 | class_tf_type.html 33 | PXR_NS::TfType::FactoryBase 34 | 35 | 36 | PXR_NS::TfType::FactoryBase 37 | class_tf_type_1_1_factory_base.html 38 | 39 | 40 | PXR_NS::TfRefBase 41 | class_tf_ref_base.html 42 | 43 | 44 | PXR_NS::TfRefPtr 45 | class_tf_ref_ptr.html 46 | 47 | 48 | PXR_NS::TfWeakBase 49 | class_tf_weak_base.html 50 | 51 | 52 | PXR_NS::TfWeakPtr 53 | class_tf_weak_ptr.html 54 | 55 | 56 | PXR_NS::TfAnyWeakPtr 57 | class_tf_any_weak_ptr.html 58 | 59 | 60 | PXR_NS::TfToken 61 | class_tf_token.html 62 | PXR_NS::TfToken::HashFunctor 63 | 64 | 65 | PXR_NS::TfToken::HashFunctor 66 | struct_tf_token_1_1_hash_functor.html 67 | 68 | 69 | PXR_NS::SdfPath 70 | class_sdf_path.html 71 | 72 | 73 | PXR_NS::UsdObject 74 | class_usd_object.html 75 | 76 | 77 | PXR_NS::UsdStage 78 | class_usd_stage.html 79 | 80 | 81 | PXR_NS::UsdStageRefPtr 82 | class_usd_stage.html 83 | 84 | 85 | PXR_NS::UsdStageWeakPtr 86 | class_usd_stage.html 87 | 88 | 89 | PXR_NS::UsdNotice 90 | class_usd_notice.html 91 | PXR_NS::UsdNotice::StageNotice 92 | PXR_NS::UsdNotice::LayerMutingChanged 93 | PXR_NS::UsdNotice::ObjectsChanged 94 | PXR_NS::UsdNotice::StageContentsChanged 95 | PXR_NS::UsdNotice::StageEditTargetChanged 96 | 97 | 98 | PXR_NS::UsdNotice::StageNotice 99 | class_usd_notice_1_1_stage_notice.html 100 | 101 | 102 | PXR_NS::UsdNotice::LayerMutingChanged 103 | class_usd_notice_1_1_layer_muting_changed.html 104 | 105 | GetMutedLayers 106 | a46081415d09be1ff2b028fd18f78ef16 107 | 108 | 109 | GetUnmutedLayers 110 | afbb387a15c8d7e1441cb4b962999aa07 111 | 112 | 113 | 114 | PXR_NS::UsdNotice::ObjectsChanged 115 | class_usd_notice_1_1_objects_changed.html 116 | 117 | AffectedObject 118 | a3c090cd8e87d62dfabf10555d6c7db13 119 | 120 | 121 | ResyncedObject 122 | abb3e5a33dc3dadb89d449d05af55cb30 123 | 124 | 125 | ChangedInfoOnly 126 | ab939047696102fb8ff522b5bbcaeb274 127 | 128 | 129 | GetResyncedPaths 130 | a793e3d781e6e01889a0ba8d789c09102 131 | 132 | 133 | GetChangedInfoOnlyPaths 134 | a235ad194cc01a9a16dabc0abe9bed144 135 | 136 | 137 | GetChangedFields 138 | a635f91971271c3fead108b2288fb0781 139 | (const UsdObject&) const 140 | 141 | 142 | GetChangedFields 143 | a6a4d7574081752d2ecc6c0aaa31d4e50 144 | (const SdfPath&) const 145 | 146 | 147 | HasChangedFields 148 | a8c1cac8459f107fde54042a657aaf440 149 | (const UsdObject&) const 150 | 151 | 152 | HasChangedFields 153 | ab10bc8abddba1c9eb3c7f864636ec1d2 154 | (const SdfPath&) const 155 | 156 | 157 | 158 | PXR_NS::UsdNotice::StageContentsChanged 159 | class_usd_notice_1_1_stage_contents_changed.html 160 | 161 | 162 | PXR_NS::UsdNotice::StageEditTargetChanged 163 | class_usd_notice_1_1_stage_edit_target_changed.html 164 | 165 | 166 | PXR_NS::SdfChangeBlock 167 | class_sdf_change_block.html 168 | 169 | 170 | -------------------------------------------------------------------------------- /doc/sphinx/getting_started.rst: -------------------------------------------------------------------------------- 1 | .. _getting_started: 2 | 3 | *************** 4 | Getting started 5 | *************** 6 | 7 | .. highlight:: python 8 | 9 | Once the library is :ref:`installed ` with Python binding, we can 10 | test the Python API by following a simple example. 11 | 12 | .. _getting_started/editing_stage: 13 | 14 | Editing the Stage 15 | ================= 16 | 17 | Let's start by creating a :term:`USD` stage in memory and register a callback 18 | to listen to :usd-cpp:`Usd.Notice.ObjectsChanged ` and print all 19 | updated paths:: 20 | 21 | from pxr import Usd, Tf 22 | 23 | stage = Usd.Stage.CreateInMemory() 24 | 25 | def _updated(notice, stage): 26 | """Print updated paths from the stage.""" 27 | print("Resynced Paths: {}".format([ 28 | (path, notice.GetChangedFields(path)) 29 | for path in notice.GetResyncedPaths() 30 | ])) 31 | print("ChangedInfoOnly Paths: {}\n".format([ 32 | (path, notice.GetChangedFields(path)) 33 | for path in notice.GetChangedInfoOnlyPaths() 34 | ])) 35 | 36 | key = Tf.Notice.Register(Usd.Notice.ObjectsChanged, _updated, stage) 37 | 38 | Let's now edit the stage by adding a cylinder prim and update the attributes:: 39 | 40 | prim = stage.DefinePrim("/Foo", "Cylinder") 41 | prim.GetAttribute("radius").Set(5) 42 | prim.GetAttribute("height").Set(10) 43 | 44 | This should have triggered five :usd-cpp:`Usd.Notice.ObjectsChanged 45 | ` notices to be emitted. The first notice was emitted when the 46 | prim was created, the second and the fourth when both attributes where created, 47 | the third and fifth when they were given a default value. As a result, the 48 | following information will be printed in the shell: 49 | 50 | .. code-block:: bash 51 | 52 | Resynced Paths: [(Sdf.Path('/Foo'), ['specifier', 'typeName'])] 53 | ChangedInfoOnly Paths: [] 54 | 55 | Resynced Paths: [(Sdf.Path('/Foo.radius'), [])] 56 | ChangedInfoOnly Paths: [] 57 | 58 | Resynced Paths: [] 59 | ChangedInfoOnly Paths: [(Sdf.Path('/Foo.radius'), ['default'])] 60 | 61 | Resynced Paths: [(Sdf.Path('/Foo.height'), [])] 62 | ChangedInfoOnly Paths: [] 63 | 64 | Resynced Paths: [] 65 | ChangedInfoOnly Paths: [(Sdf.Path('/Foo.height'), ['default'])] 66 | 67 | .. _getting_started/editing_layer: 68 | 69 | Editing the Layer 70 | ================= 71 | 72 | To consolidate the number of notices emitted, we could use the :term:`Sdf` API 73 | to edit the root layer, then use a :usd-cpp:`Sdf.ChangeBlock ` 74 | which would also limit the number of recompositions and greatly improve overall 75 | performance:: 76 | 77 | from pxr import Sdf 78 | 79 | layer = stage.GetRootLayer() 80 | 81 | with Sdf.ChangeBlock(): 82 | prim = Sdf.CreatePrimInLayer(layer, "/Foo") 83 | prim.specifier = Sdf.SpecifierDef 84 | prim.typeName = "Cylinder" 85 | 86 | attr = Sdf.AttributeSpec(prim, "radius", Sdf.ValueTypeNames.Double) 87 | attr.default = 5 88 | 89 | attr = Sdf.AttributeSpec(prim, "height", Sdf.ValueTypeNames.Double) 90 | attr.default = 10 91 | 92 | .. warning:: 93 | 94 | It is not safe to edit the stage with the :term:`USD` API when using 95 | :usd-cpp:`Sdf.ChangeBlock `. 96 | 97 | One single notice will be emitted: 98 | 99 | .. code-block:: bash 100 | 101 | Resynced Paths: [(Sdf.Path('/Foo'), ['specifier', 'typeName'])] 102 | ChangedInfoOnly Paths: [] 103 | 104 | .. _getting_started/using: 105 | 106 | Using the library 107 | ================= 108 | 109 | Let's now create a new stage and modify the notice registration to target the 110 | :class:`unf.Notice.ObjectsChanged` notice: 111 | 112 | .. code-block:: python 113 | :emphasize-lines: 2,17 114 | 115 | from pxr import Usd, Tf 116 | import unf 117 | 118 | stage = Usd.Stage.CreateInMemory() 119 | 120 | def _updated(notice, stage): 121 | """Print updated paths from the stage.""" 122 | print("Resynced Paths: {}".format([ 123 | (path, notice.GetChangedFields(path)) 124 | for path in notice.GetResyncedPaths() 125 | ])) 126 | print("ChangedInfoOnly Paths: {}\n".format([ 127 | (path, notice.GetChangedFields(path)) 128 | for path in notice.GetChangedInfoOnlyPaths() 129 | ])) 130 | 131 | key = Tf.Notice.Register(unf.Notice.ObjectsChanged, _updated, stage) 132 | 133 | To ensure that a :class:`unf.Notice.ObjectsChanged` notice is sent whenever a 134 | :usd-cpp:`Usd.Notice.ObjectsChanged ` is emitted, we need to 135 | create a :class:`~unf.Broker` associated with the stage:: 136 | 137 | broker = unf.Broker.Create(stage) 138 | 139 | .. note:: 140 | 141 | The :class:`~unf.Broker` is using a :ref:`dispatcher ` to 142 | emit a standalone notice for each Usd notice. 143 | 144 | Let's now edit the stage once again with the :term:`USD` API:: 145 | 146 | prim = stage.DefinePrim("/Foo", "Cylinder") 147 | prim.GetAttribute("radius").Set(5) 148 | prim.GetAttribute("height").Set(10) 149 | 150 | Like in the first section, five notices are emitted with the same information 151 | as with the :usd-cpp:`Usd.Notice.ObjectsChanged ` notice. 152 | However, the :class:`unf.Notice.ObjectsChanged` notice is defined as mergeable. 153 | It is therefore possible to reduce the number of notices emitted by using a 154 | :ref:`notice transaction `:: 155 | 156 | broker.BeginTransaction() 157 | 158 | prim = stage.DefinePrim("/Foo", "Cylinder") 159 | prim.GetAttribute("radius").Set(5) 160 | prim.GetAttribute("height").Set(10) 161 | 162 | broker.EndTransaction() 163 | 164 | For safety, it is recommended to use the :class:`unf.NoticeTransaction` object 165 | instead which can be used as a context manager:: 166 | 167 | with unf.NoticeTransaction(broker): 168 | prim = stage.DefinePrim("/Foo", "Cylinder") 169 | prim.GetAttribute("radius").Set(5) 170 | prim.GetAttribute("height").Set(10) 171 | 172 | As a result, only one notice will be emitted: 173 | 174 | .. code-block:: bash 175 | 176 | Resynced Paths: [(Sdf.Path('/Foo'), ['typeName', 'specifier'])] 177 | ChangedInfoOnly Paths: [] 178 | 179 | It is sometimes necessary to de-register listeners to a particular set of 180 | notices when editing the stage. If many clients are listening to Usd notices, 181 | this process can be tedious. 182 | 183 | The Unf library provides a way to filter out some or all Unf notices during a 184 | transaction using a predicate function. For instance, the following transaction 185 | will only emit "Foo" notices:: 186 | 187 | def _predicate(notice): 188 | """Indicate whether *notice* should be captured and emitted.""" 189 | return isinstance(notice, Foo) 190 | 191 | with unf.NoticeTransaction(stage, predicate=_predicate): 192 | # Stage editing ... 193 | 194 | For convenience, a :meth:`unf.CapturePredicate.BlockAll` predicate has been 195 | provided to block all notices emitted during a transaction:: 196 | 197 | with unf.NoticeTransaction( 198 | stage, predicate=unf.CapturePredicate.BlockAll() 199 | ): 200 | # Stage editing ... 201 | 202 | .. seealso:: :ref:`notices` 203 | -------------------------------------------------------------------------------- /src/unf/notice.cpp: -------------------------------------------------------------------------------- 1 | #include "unf/notice.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | PXR_NAMESPACE_USING_DIRECTIVE 11 | 12 | namespace unf { 13 | 14 | namespace UnfNotice { 15 | 16 | TF_REGISTRY_FUNCTION(TfType) 17 | { 18 | TfType::Define >(); 19 | 20 | TfType::Define >(); 21 | TfType::Define >(); 22 | TfType::Define >(); 23 | TfType::Define >(); 24 | } 25 | 26 | ObjectsChanged::ObjectsChanged(const UsdNotice::ObjectsChanged& notice) 27 | { 28 | // TODO: Update Usd Notice to give easier access to fields. 29 | 30 | for (const auto& path : notice.GetResyncedPaths()) { 31 | _resyncChanges.push_back(path); 32 | 33 | auto tokens = notice.GetChangedFields(path); 34 | if (tokens.size() > 0) { 35 | _changedFields[path] = TfTokenSet(tokens.begin(), tokens.end()); 36 | } 37 | } 38 | for (const auto& path : notice.GetChangedInfoOnlyPaths()) { 39 | _infoChanges.push_back(path); 40 | 41 | auto tokens = notice.GetChangedFields(path); 42 | if (tokens.size() > 0) { 43 | _changedFields[path] = TfTokenSet(tokens.begin(), tokens.end()); 44 | } 45 | } 46 | } 47 | 48 | ObjectsChanged::ObjectsChanged(const ObjectsChanged& other) 49 | : _resyncChanges(other._resyncChanges), 50 | _infoChanges(other._infoChanges), 51 | _changedFields(other._changedFields) 52 | { 53 | } 54 | 55 | ObjectsChanged& ObjectsChanged::operator=(const ObjectsChanged& other) 56 | { 57 | ObjectsChanged copy(other); 58 | std::swap(_resyncChanges, copy._resyncChanges); 59 | std::swap(_infoChanges, copy._infoChanges); 60 | std::swap(_changedFields, copy._changedFields); 61 | return *this; 62 | } 63 | 64 | void ObjectsChanged::Merge(ObjectsChanged&& notice) 65 | { 66 | // Update resyncChanges if necessary. 67 | for (auto& path : notice._resyncChanges) { 68 | const auto iter = 69 | std::find(_resyncChanges.begin(), _resyncChanges.end(), path); 70 | if (iter == _resyncChanges.end()) { 71 | _resyncChanges.emplace_back(std::move(path)); 72 | } 73 | } 74 | 75 | // Update infoChanges if necessary. 76 | for (const auto& path : notice._infoChanges) { 77 | const SdfPath& primPath = path.GetPrimPath(); 78 | 79 | // Skip if the path is already in resyncedPaths. 80 | { 81 | const auto it = std::find( 82 | _resyncChanges.begin(), _resyncChanges.end(), primPath); 83 | if (it != _resyncChanges.end()) continue; 84 | } 85 | 86 | // Skip if an ancestor of the path is already in resyncedPaths. 87 | bool ancestorResynced = false; 88 | for (const auto& ancestor : primPath.GetPrefixes()) { 89 | const auto it = std::find( 90 | _resyncChanges.begin(), _resyncChanges.end(), ancestor); 91 | if (it != _resyncChanges.end()) { 92 | goto continue_ancestorResynced; 93 | } 94 | } 95 | 96 | // Add infoChanges, when not already available 97 | { 98 | const auto it = 99 | std::find(_infoChanges.begin(), _infoChanges.end(), path); 100 | if (it == _infoChanges.end()) { 101 | _infoChanges.push_back(std::move(path)); 102 | } 103 | } 104 | continue_ancestorResynced:; 105 | } 106 | 107 | // Update changeFields. 108 | for (auto const& entry : notice._changedFields) { 109 | auto const& path = entry.first; 110 | 111 | if (_changedFields.find(path) == _changedFields.end()) { 112 | _changedFields[path] = std::move(notice._changedFields[path]); 113 | } 114 | else { 115 | _changedFields[path].insert( 116 | notice._changedFields[path].begin(), 117 | notice._changedFields[path].end()); 118 | } 119 | } 120 | } 121 | 122 | void ObjectsChanged::PostProcess() 123 | { 124 | SdfPath::RemoveDescendentPaths(&_resyncChanges); 125 | } 126 | 127 | bool ObjectsChanged::ResyncedObject(const PXR_NS::UsdObject& object) const 128 | { 129 | auto path = PXR_NS::SdfPathFindLongestPrefix( 130 | _resyncChanges.begin(), _resyncChanges.end(), object.GetPath()); 131 | return path != _resyncChanges.end(); 132 | } 133 | 134 | bool ObjectsChanged::ChangedInfoOnly(const PXR_NS::UsdObject& object) const 135 | { 136 | auto path = PXR_NS::SdfPathFindLongestPrefix( 137 | _infoChanges.begin(), _infoChanges.end(), object.GetPath()); 138 | return path != _infoChanges.end(); 139 | } 140 | 141 | TfTokenSet ObjectsChanged::GetChangedFields( 142 | const PXR_NS::UsdObject& object) const 143 | { 144 | return GetChangedFields(object.GetPath()); 145 | } 146 | 147 | TfTokenSet ObjectsChanged::GetChangedFields(const PXR_NS::SdfPath& path) const 148 | { 149 | if (HasChangedFields(path)) { 150 | return _changedFields.at(path); 151 | } 152 | return TfTokenSet(); 153 | } 154 | 155 | bool ObjectsChanged::HasChangedFields(const UsdObject& object) const 156 | { 157 | return HasChangedFields(object.GetPath()); 158 | } 159 | 160 | bool ObjectsChanged::HasChangedFields(const SdfPath& path) const 161 | { 162 | if (_changedFields.find(path) != _changedFields.end()) { 163 | return true; 164 | } 165 | 166 | return false; 167 | } 168 | 169 | LayerMutingChanged::LayerMutingChanged( 170 | const UsdNotice::LayerMutingChanged& notice) 171 | { 172 | for (const auto& layer : notice.GetMutedLayers()) { 173 | _mutedLayers.push_back(layer); 174 | } 175 | 176 | for (const auto& layer : notice.GetUnmutedLayers()) { 177 | _unmutedLayers.push_back(layer); 178 | } 179 | } 180 | 181 | LayerMutingChanged::LayerMutingChanged(const LayerMutingChanged& other) 182 | : _mutedLayers(other._mutedLayers), _unmutedLayers(other._unmutedLayers) 183 | { 184 | } 185 | 186 | LayerMutingChanged& LayerMutingChanged::operator=( 187 | const LayerMutingChanged& other) 188 | { 189 | LayerMutingChanged copy(other); 190 | std::swap(_mutedLayers, copy._mutedLayers); 191 | std::swap(_unmutedLayers, copy._unmutedLayers); 192 | return *this; 193 | } 194 | 195 | void LayerMutingChanged::Merge(LayerMutingChanged&& notice) 196 | { 197 | size_t mutedLayersSize = _mutedLayers.size(); 198 | size_t unmutedLayersSize = _unmutedLayers.size(); 199 | 200 | for (const auto& layer : notice._mutedLayers) { 201 | auto begin = _unmutedLayers.begin(); 202 | auto end = begin + unmutedLayersSize; 203 | auto it = std::find(begin, end, layer); 204 | if (it != end) { 205 | _unmutedLayers.erase(it); 206 | unmutedLayersSize -= 1; 207 | } 208 | else { 209 | _mutedLayers.push_back(std::move(layer)); 210 | } 211 | } 212 | 213 | for (const auto& layer : notice._unmutedLayers) { 214 | auto begin = _mutedLayers.begin(); 215 | auto end = begin + mutedLayersSize; 216 | auto it = std::find(begin, end, layer); 217 | if (it != end) { 218 | _mutedLayers.erase(it); 219 | mutedLayersSize -= 1; 220 | } 221 | else { 222 | _unmutedLayers.push_back(std::move(layer)); 223 | } 224 | } 225 | } 226 | 227 | } // namespace UnfNotice 228 | 229 | } // namespace unf 230 | -------------------------------------------------------------------------------- /doc/sphinx/notices.rst: -------------------------------------------------------------------------------- 1 | .. _notices: 2 | 3 | *********************** 4 | Using Standalone Notice 5 | *********************** 6 | 7 | The USD Notice Framework provides a :unf-cpp:`UnfNotice::StageNotice` interface 8 | which can be used to create standalone notices related to the :term:`USD` stage. 9 | 10 | Standalone notices present the following features: 11 | 12 | 1. **They do not reference data from the stage** 13 | 14 | This rule ensures that notices are safe to use in asynchronous context. 15 | By contrast, Usd notices such as :usd-cpp:`UsdNotice::ObjectsChanged` and 16 | :usd-cpp:`UsdNotice::LayerMutingChanged` both reference data from the stage and 17 | thus are not safe to use when the stage is not longer reachable. 18 | 19 | 2. **They can include logic for consolidation with notice of the same type** 20 | 21 | This makes it possible to reduce the number of notices emitted during a 22 | transaction without loosing information. 23 | 24 | .. _notices/transaction: 25 | 26 | Using notice transaction 27 | ======================== 28 | 29 | A transaction is a time frame during which the emission of standalone notices 30 | needs to be withheld. The :unf-cpp:`Broker` is in charge of capturing these 31 | notices via a :unf-cpp:`NoticeTransaction` instance: 32 | 33 | .. code-block:: cpp 34 | 35 | auto stage = PXR_NS::UsdStage::CreateInMemory(); 36 | auto broker = unf::Broker::Create(stage); 37 | 38 | { 39 | unf::NoticeTransaction transaction(broker); 40 | 41 | // Emission of standalone notices is deferred until the end of the 42 | // transaction. Other notices are sent as normal. 43 | } 44 | 45 | A :unf-cpp:`NoticeTransaction` instance can also be constructed directly from 46 | the Usd stage, which encapsulates the creation of the broker if none have been 47 | previously created for this stage: 48 | 49 | .. code-block:: cpp 50 | 51 | auto stage = PXR_NS::UsdStage::CreateInMemory(); 52 | 53 | { 54 | unf::NoticeTransaction transaction(stage); 55 | 56 | // ... 57 | } 58 | 59 | At the end of a transaction, all notices captured are emitted. for a standalone 60 | notice to be captured, it needs to be sent via the :unf-cpp:`Broker`. Let's 61 | consider a ficticious standalone notice named "Foo". It can be created and sent 62 | with this templated method: 63 | 64 | .. code-block:: cpp 65 | 66 | auto broker = unf::Broker::Create(stage); 67 | broker->Send() 68 | 69 | It can also be created separately and sent as follows: 70 | 71 | .. code-block:: cpp 72 | 73 | auto broker = unf::Broker::Create(stage); 74 | auto notice = Foo::Create(); 75 | 76 | broker->Send(notice); 77 | 78 | .. warning:: 79 | 80 | If the notice is sent as follows, it will not be captured by the broker: 81 | 82 | .. code-block:: cpp 83 | 84 | auto notice = Foo::Create(); 85 | notice->Send(); 86 | 87 | Standalone notices cannot be sent in Python. 88 | 89 | .. note:: 90 | 91 | The sending process is usually handled by a :ref:`Dispatcher `. 92 | 93 | A notice can be defined as "mergeable" or "unmergeable". If a notice is defined 94 | as unmergeable, no consolidation will take place during a transaction. In the 95 | following example, one consolidated "Foo" notice will be sent at the end 96 | of the transaction if the notice was mergeable. Otherwise, the three notices 97 | are sent: 98 | 99 | .. code-block:: cpp 100 | 101 | auto stage = PXR_NS::UsdStage::CreateInMemory(); 102 | auto broker = unf::Broker::Create(stage); 103 | 104 | auto notice = Foo::Create(); 105 | 106 | // Indicate whether the notice can be merged. 107 | printf(notice->IsMergeable()) 108 | 109 | { 110 | unf::NoticeTransaction transaction(broker); 111 | 112 | // The following notices will be captured by the broker during the 113 | // scope of the transaction. 114 | broker->Send(notice); 115 | broker->Send(notice); 116 | broker->Send(notice); 117 | } 118 | 119 | It is possible to start the transaction with a predicate function to indicate 120 | which notices are captured during the transaction. The following example will 121 | only filter in the "Foo" notices: 122 | 123 | .. code-block:: cpp 124 | 125 | auto predicate = [&](const unf::UnfNotice::StageNotice& notice) { 126 | return (typeid(notice).name() == typeid(Foo).name()); 127 | }; 128 | 129 | { 130 | unf::NoticeTransaction transaction(broker, predicate); 131 | 132 | // ... 133 | } 134 | 135 | For convenience, a predicate has been provided to block all notices emitted 136 | during a transaction: 137 | 138 | .. code-block:: cpp 139 | 140 | { 141 | unf::NoticeTransaction transaction( 142 | broker, unf::CapturePredicate::BlockAll()); 143 | 144 | // ... 145 | } 146 | 147 | .. _notices/default: 148 | 149 | Default notices 150 | =============== 151 | 152 | By default, the broker will emit standalone equivalents for each :term:`USD` 153 | notices: 154 | 155 | ============================================= ============================================= 156 | Usd notices Standalone Notices 157 | ============================================= ============================================= 158 | :usd-cpp:`UsdNotice::ObjectsChanged` :unf-cpp:`UnfNotice::ObjectsChanged` 159 | :usd-cpp:`UsdNotice::LayerMutingChanged` :unf-cpp:`UnfNotice::LayerMutingChanged` 160 | :usd-cpp:`UsdNotice::StageContentsChanged` :unf-cpp:`UnfNotice::StageContentsChanged` 161 | :usd-cpp:`UsdNotice::StageEditTargetChanged` :unf-cpp:`UnfNotice::StageEditTargetChanged` 162 | ============================================= ============================================= 163 | 164 | Python bindings are also provided for each notice: 165 | 166 | * :class:`~unf.Notice.ObjectsChanged` 167 | * :class:`~unf.Notice.LayerMutingChanged` 168 | * :class:`~unf.Notice.StageContentsChanged` 169 | * :class:`~unf.Notice.StageEditTargetChanged` 170 | 171 | All of these notices are defined as mergeable and therefore will be 172 | consolidated per notice type during a transaction. 173 | 174 | .. note:: 175 | 176 | These notices are handled by the :ref:`StageDispatcher `. 177 | 178 | .. _notices/custom: 179 | 180 | Custom notices 181 | ============== 182 | 183 | The :unf-cpp:`UnfNotice::StageNotice` interface can be safely derived as follows 184 | to create new notices: 185 | 186 | .. code-block:: cpp 187 | 188 | class Foo : public unf::UnfNotice::StageNoticeImpl { 189 | public: 190 | Foo() = default; 191 | virtual ~Foo() = default; 192 | }; 193 | 194 | By default, this notice will be mergeable, it can be made unmergeable as 195 | follows: 196 | 197 | .. code-block:: cpp 198 | 199 | class Foo : public unf::UnfNotice::StageNoticeImpl { 200 | public: 201 | Foo() = default; 202 | virtual ~Foo() = default; 203 | 204 | bool IsMergeable() const override { return false; } 205 | }; 206 | 207 | If the notice is mergeable and contain some data, the "Merge" method needs 208 | to be implemented to indicate how notices are consolidated. The "PostProcess" 209 | method could also be implemented to process the data after it has been merged 210 | with other notices: 211 | 212 | .. code-block:: cpp 213 | 214 | using DataMap = std::unordered_map; 215 | 216 | class Foo : public unf::UnfNotice::StageNoticeImpl { 217 | public: 218 | Foo() = default; 219 | virtual ~Foo() = default; 220 | 221 | void Merge(Foo&& notice) override 222 | { 223 | for (const auto& it : notice._data) { 224 | _data[it.first] = std::move(it.second); 225 | } 226 | } 227 | 228 | void PostProcess() override 229 | { 230 | // ... 231 | } 232 | 233 | private: 234 | DataMap _data; 235 | }; 236 | 237 | 238 | .. note:: 239 | 240 | The copy constructor and assignment operator should be implemented as well 241 | if the notice contains data. 242 | 243 | .. warning:: 244 | 245 | Custom standalone notices cannot be implemented in Python. 246 | -------------------------------------------------------------------------------- /test/unit/testBrokerFlow.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | class BrokerFlowTest : public ::testing::Test { 12 | protected: 13 | using Listener = 14 | ::Test::Listener<::Test::MergeableNotice, ::Test::UnMergeableNotice>; 15 | 16 | void SetUp() override 17 | { 18 | _stage = PXR_NS::UsdStage::CreateInMemory(); 19 | _listener.SetStage(_stage); 20 | } 21 | 22 | PXR_NS::UsdStageRefPtr _stage; 23 | Listener _listener; 24 | }; 25 | 26 | TEST_F(BrokerFlowTest, Send) 27 | { 28 | auto broker = unf::Broker::Create(_stage); 29 | 30 | broker->Send<::Test::MergeableNotice>(); 31 | broker->Send<::Test::MergeableNotice>(); 32 | broker->Send<::Test::MergeableNotice>(); 33 | 34 | broker->Send<::Test::UnMergeableNotice>(); 35 | broker->Send<::Test::UnMergeableNotice>(); 36 | broker->Send<::Test::UnMergeableNotice>(); 37 | 38 | ASSERT_EQ(_listener.Received<::Test::MergeableNotice>(), 3); 39 | ASSERT_EQ(_listener.Received<::Test::UnMergeableNotice>(), 3); 40 | } 41 | 42 | TEST_F(BrokerFlowTest, Transaction) 43 | { 44 | auto broker = unf::Broker::Create(_stage); 45 | 46 | ASSERT_FALSE(broker->IsInTransaction()); 47 | 48 | broker->BeginTransaction(); 49 | ASSERT_TRUE(broker->IsInTransaction()); 50 | 51 | broker->Send<::Test::MergeableNotice>(); 52 | broker->Send<::Test::MergeableNotice>(); 53 | broker->Send<::Test::MergeableNotice>(); 54 | 55 | broker->Send<::Test::UnMergeableNotice>(); 56 | broker->Send<::Test::UnMergeableNotice>(); 57 | broker->Send<::Test::UnMergeableNotice>(); 58 | 59 | // No notices are emitted during a transaction. 60 | ASSERT_EQ(_listener.Received<::Test::MergeableNotice>(), 0); 61 | ASSERT_EQ(_listener.Received<::Test::UnMergeableNotice>(), 0); 62 | 63 | broker->EndTransaction(); 64 | ASSERT_FALSE(broker->IsInTransaction()); 65 | 66 | // Consolidated notices (if required) are sent when transaction is over. 67 | ASSERT_EQ(_listener.Received<::Test::MergeableNotice>(), 1); 68 | ASSERT_EQ(_listener.Received<::Test::UnMergeableNotice>(), 3); 69 | } 70 | 71 | TEST_F(BrokerFlowTest, NestedTransaction) 72 | { 73 | auto broker = unf::Broker::Create(_stage); 74 | 75 | ASSERT_FALSE(broker->IsInTransaction()); 76 | 77 | broker->BeginTransaction(); 78 | ASSERT_TRUE(broker->IsInTransaction()); 79 | 80 | broker->Send<::Test::MergeableNotice>(); 81 | broker->Send<::Test::MergeableNotice>(); 82 | broker->Send<::Test::MergeableNotice>(); 83 | 84 | broker->Send<::Test::UnMergeableNotice>(); 85 | broker->Send<::Test::UnMergeableNotice>(); 86 | broker->Send<::Test::UnMergeableNotice>(); 87 | 88 | broker->BeginTransaction(); 89 | ASSERT_TRUE(broker->IsInTransaction()); 90 | 91 | broker->Send<::Test::MergeableNotice>(); 92 | broker->Send<::Test::MergeableNotice>(); 93 | broker->Send<::Test::MergeableNotice>(); 94 | 95 | broker->Send<::Test::UnMergeableNotice>(); 96 | broker->Send<::Test::UnMergeableNotice>(); 97 | broker->Send<::Test::UnMergeableNotice>(); 98 | 99 | broker->EndTransaction(); 100 | ASSERT_TRUE(broker->IsInTransaction()); 101 | 102 | // No notices are emitted while at least one transaction is started. 103 | ASSERT_EQ(_listener.Received<::Test::MergeableNotice>(), 0); 104 | ASSERT_EQ(_listener.Received<::Test::UnMergeableNotice>(), 0); 105 | 106 | broker->EndTransaction(); 107 | ASSERT_FALSE(broker->IsInTransaction()); 108 | 109 | // Consolidated notices (if required) are sent when all 110 | // transactions are over. 111 | ASSERT_EQ(_listener.Received<::Test::MergeableNotice>(), 1); 112 | ASSERT_EQ(_listener.Received<::Test::UnMergeableNotice>(), 6); 113 | } 114 | 115 | TEST_F(BrokerFlowTest, MergeableNotice) 116 | { 117 | auto broker = unf::Broker::Create(_stage); 118 | 119 | ::Test::Observer<::Test::MergeableNotice> observer(_stage); 120 | 121 | ASSERT_FALSE(broker->IsInTransaction()); 122 | 123 | broker->BeginTransaction(); 124 | ASSERT_TRUE(broker->IsInTransaction()); 125 | 126 | broker->Send<::Test::MergeableNotice>(::Test::DataMap({{"Foo", "Test1"}})); 127 | broker->Send<::Test::MergeableNotice>(::Test::DataMap({{"Foo", "Test2"}})); 128 | broker->Send<::Test::MergeableNotice>(::Test::DataMap({{"Bar", "Test3"}})); 129 | 130 | broker->EndTransaction(); 131 | ASSERT_FALSE(broker->IsInTransaction()); 132 | 133 | // Ensure that only one consolidated notice is received. 134 | ASSERT_EQ(_listener.Received<::Test::MergeableNotice>(), 1); 135 | 136 | const auto& n = observer.GetLatestNotice(); 137 | ASSERT_EQ( 138 | n.GetData(), ::Test::DataMap({{"Foo", "Test2"}, {"Bar", "Test3"}})); 139 | } 140 | 141 | TEST_F(BrokerFlowTest, WithFilter) 142 | { 143 | auto broker = unf::Broker::Create(_stage); 144 | 145 | ASSERT_FALSE(broker->IsInTransaction()); 146 | 147 | // Filter out UnMergeableNotice type. 148 | std::string target = typeid(::Test::UnMergeableNotice).name(); 149 | auto predicate = [&](const unf::UnfNotice::StageNotice& n) { 150 | return (typeid(n).name() != target); 151 | }; 152 | 153 | broker->BeginTransaction(predicate); 154 | ASSERT_TRUE(broker->IsInTransaction()); 155 | 156 | broker->Send<::Test::MergeableNotice>(); 157 | broker->Send<::Test::MergeableNotice>(); 158 | broker->Send<::Test::MergeableNotice>(); 159 | 160 | broker->Send<::Test::UnMergeableNotice>(); 161 | broker->Send<::Test::UnMergeableNotice>(); 162 | broker->Send<::Test::UnMergeableNotice>(); 163 | 164 | // No notices are emitted during a transaction. 165 | ASSERT_EQ(_listener.Received<::Test::MergeableNotice>(), 0); 166 | ASSERT_EQ(_listener.Received<::Test::UnMergeableNotice>(), 0); 167 | 168 | broker->EndTransaction(); 169 | ASSERT_FALSE(broker->IsInTransaction()); 170 | 171 | // Consolidated notices (if required) are sent when transaction is over. 172 | ASSERT_EQ(_listener.Received<::Test::MergeableNotice>(), 1); 173 | ASSERT_EQ(_listener.Received<::Test::UnMergeableNotice>(), 0); 174 | } 175 | 176 | TEST_F(BrokerFlowTest, WithDefaultPredicate) 177 | { 178 | auto broker = unf::Broker::Create(_stage); 179 | 180 | ASSERT_FALSE(broker->IsInTransaction()); 181 | 182 | broker->BeginTransaction(unf::CapturePredicate::Default()); 183 | ASSERT_TRUE(broker->IsInTransaction()); 184 | 185 | broker->Send<::Test::MergeableNotice>(); 186 | broker->Send<::Test::MergeableNotice>(); 187 | broker->Send<::Test::MergeableNotice>(); 188 | 189 | broker->Send<::Test::UnMergeableNotice>(); 190 | broker->Send<::Test::UnMergeableNotice>(); 191 | broker->Send<::Test::UnMergeableNotice>(); 192 | 193 | // No notices are emitted during a transaction. 194 | ASSERT_EQ(_listener.Received<::Test::MergeableNotice>(), 0); 195 | ASSERT_EQ(_listener.Received<::Test::UnMergeableNotice>(), 0); 196 | 197 | broker->EndTransaction(); 198 | ASSERT_FALSE(broker->IsInTransaction()); 199 | 200 | // No notices are emitted after the transaction either. 201 | ASSERT_EQ(_listener.Received<::Test::MergeableNotice>(), 1); 202 | ASSERT_EQ(_listener.Received<::Test::UnMergeableNotice>(), 3); 203 | } 204 | 205 | TEST_F(BrokerFlowTest, WithBlockAllPredicate) 206 | { 207 | auto broker = unf::Broker::Create(_stage); 208 | 209 | ASSERT_FALSE(broker->IsInTransaction()); 210 | 211 | broker->BeginTransaction(unf::CapturePredicate::BlockAll()); 212 | ASSERT_TRUE(broker->IsInTransaction()); 213 | 214 | broker->Send<::Test::MergeableNotice>(); 215 | broker->Send<::Test::MergeableNotice>(); 216 | broker->Send<::Test::MergeableNotice>(); 217 | 218 | broker->Send<::Test::UnMergeableNotice>(); 219 | broker->Send<::Test::UnMergeableNotice>(); 220 | broker->Send<::Test::UnMergeableNotice>(); 221 | 222 | // No notices are emitted during a transaction. 223 | ASSERT_EQ(_listener.Received<::Test::MergeableNotice>(), 0); 224 | ASSERT_EQ(_listener.Received<::Test::UnMergeableNotice>(), 0); 225 | 226 | broker->EndTransaction(); 227 | ASSERT_FALSE(broker->IsInTransaction()); 228 | 229 | // No notices are emitted after the transaction either. 230 | ASSERT_EQ(_listener.Received<::Test::MergeableNotice>(), 0); 231 | ASSERT_EQ(_listener.Received<::Test::UnMergeableNotice>(), 0); 232 | } 233 | -------------------------------------------------------------------------------- /test/integration/python/test_mute_layers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pxr import Usd, Sdf, Tf 4 | import unf 5 | 6 | import pytest 7 | 8 | 9 | @pytest.mark.parametrize("notice_type, excepted", [ 10 | ("StageNotice", 12), 11 | ("StageContentsChanged", 4), 12 | ("ObjectsChanged", 4), 13 | ("StageEditTargetChanged", 0), 14 | ("LayerMutingChanged", 4), 15 | ], ids=[ 16 | "StageNotice", 17 | "StageContentsChanged", 18 | "ObjectsChanged", 19 | "StageEditTargetChanged", 20 | "LayerMutingChanged", 21 | ]) 22 | def test_mute_layers(notice_type, excepted, stage_with_layers): 23 | """Mute several layers. 24 | """ 25 | stage = stage_with_layers 26 | unf.Broker.Create(stage) 27 | 28 | # Listen to broker notice. 29 | received_broker = [] 30 | key1 = Tf.Notice.Register( 31 | getattr(unf.Notice, notice_type), 32 | lambda n, _: received_broker.append(n), stage) 33 | 34 | # Listen to corresponding USD notice. 35 | received_usd = [] 36 | key2 = Tf.Notice.Register( 37 | getattr(Usd.Notice, notice_type), 38 | lambda n, _: received_usd.append(n), stage) 39 | 40 | layers = stage.GetRootLayer().subLayerPaths 41 | 42 | # Keep ref pointer to the layer we try to mute and unmute to 43 | # prevent it for being destroyed when it is muted. 44 | _layer = Sdf.Layer.FindOrOpen(layers[1]) 45 | 46 | stage.MuteLayer(layers[0]) 47 | stage.MuteLayer(layers[1]) 48 | stage.UnmuteLayer(layers[1]) 49 | stage.MuteAndUnmuteLayers([layers[2], layers[1]], []) 50 | 51 | # Ensure that we received the same number of notices. 52 | assert len(received_broker) == excepted 53 | assert len(received_usd) == excepted 54 | 55 | @pytest.mark.parametrize("notice_type, expected_usd, expected_broker", [ 56 | ("StageNotice", 12, 3), 57 | ("StageContentsChanged", 4, 1), 58 | ("ObjectsChanged", 4, 1), 59 | ("StageEditTargetChanged", 0, 0), 60 | ("LayerMutingChanged", 4, 1), 61 | ], ids=[ 62 | "StageNotice", 63 | "StageContentsChanged", 64 | "ObjectsChanged", 65 | "StageEditTargetChanged", 66 | "LayerMutingChanged", 67 | ]) 68 | def test_mute_layers_batching( 69 | notice_type, expected_usd, expected_broker, stage_with_layers 70 | ): 71 | """Mute several layers and batch broker notices. 72 | """ 73 | stage = stage_with_layers 74 | broker = unf.Broker.Create(stage) 75 | 76 | # Listen to broker notice. 77 | received_broker = [] 78 | key1 = Tf.Notice.Register( 79 | getattr(unf.Notice, notice_type), 80 | lambda n, _: received_broker.append(n), stage) 81 | 82 | # Listen to corresponding USD notice. 83 | received_usd = [] 84 | key2 = Tf.Notice.Register( 85 | getattr(Usd.Notice, notice_type), 86 | lambda n, _: received_usd.append(n), stage) 87 | 88 | broker.BeginTransaction() 89 | 90 | layers = stage.GetRootLayer().subLayerPaths 91 | 92 | # Keep ref pointer to the layer we try to mute and unmute to 93 | # prevent it for being destroyed when it is muted. 94 | _layer = Sdf.Layer.FindOrOpen(layers[1]) 95 | 96 | stage.MuteLayer(layers[0]) 97 | stage.MuteLayer(layers[1]) 98 | stage.UnmuteLayer(layers[1]) 99 | stage.MuteAndUnmuteLayers([layers[2], layers[1]], []) 100 | 101 | # Ensure that broker notices are not sent during a transaction. 102 | assert len(received_broker) == 0 103 | 104 | # While USD Notices are being sent as expected. 105 | assert len(received_usd) == expected_usd 106 | 107 | broker.EndTransaction() 108 | 109 | # Ensure that consolidated broker notices are sent after a transaction. 110 | assert len(received_broker) == expected_broker 111 | 112 | @pytest.mark.parametrize("notice_type, expected_usd", [ 113 | ("StageNotice", 12), 114 | ("StageContentsChanged", 4), 115 | ("ObjectsChanged", 4), 116 | ("StageEditTargetChanged", 0), 117 | ("LayerMutingChanged", 4), 118 | ], ids=[ 119 | "StageNotice", 120 | "StageContentsChanged", 121 | "ObjectsChanged", 122 | "StageEditTargetChanged", 123 | "LayerMutingChanged", 124 | ]) 125 | def test_mute_layers_blocking( 126 | notice_type, expected_usd, stage_with_layers 127 | ): 128 | """Mute several layers and block broker notices. 129 | """ 130 | stage = stage_with_layers 131 | broker = unf.Broker.Create(stage) 132 | 133 | # Listen to broker notice. 134 | received_broker = [] 135 | key1 = Tf.Notice.Register( 136 | getattr(unf.Notice, notice_type), 137 | lambda n, _: received_broker.append(n), stage) 138 | 139 | # Listen to corresponding USD notice. 140 | received_usd = [] 141 | key2 = Tf.Notice.Register( 142 | getattr(Usd.Notice, notice_type), 143 | lambda n, _: received_usd.append(n), stage) 144 | 145 | # Predicate blocking all broker notices. 146 | broker.BeginTransaction(predicate=lambda _: False) 147 | 148 | layers = stage.GetRootLayer().subLayerPaths 149 | 150 | # Keep ref pointer to the layer we try to mute and unmute to 151 | # prevent it for being destroyed when it is muted. 152 | _layer = Sdf.Layer.FindOrOpen(layers[1]) 153 | 154 | stage.MuteLayer(layers[0]) 155 | stage.MuteLayer(layers[1]) 156 | stage.UnmuteLayer(layers[1]) 157 | stage.MuteAndUnmuteLayers([layers[2], layers[1]], []) 158 | 159 | # Ensure that broker notices are not sent during a transaction. 160 | assert len(received_broker) == 0 161 | 162 | # While USD Notices are being sent as expected. 163 | assert len(received_usd) == expected_usd 164 | 165 | broker.EndTransaction() 166 | 167 | # Ensure that no broker notices have been received. 168 | assert len(received_broker) == 0 169 | 170 | def test_mute_layers_transaction_objectschanged(stage_with_layers): 171 | """Mute several layers during transaction and analyze ObjectsChanged notice. 172 | """ 173 | stage = stage_with_layers 174 | broker = unf.Broker.Create(stage) 175 | 176 | received = [] 177 | 178 | def _validate(notice, stage): 179 | """Validate notice received.""" 180 | assert len(notice.GetResyncedPaths()) == 1 181 | assert len(notice.GetChangedInfoOnlyPaths()) == 0 182 | assert notice.GetResyncedPaths()[0] == "/" 183 | received.append(notice) 184 | 185 | # Create prim before caching to trigger resync path when muting. 186 | layers = stage.GetRootLayer().subLayerPaths 187 | layer = Sdf.Layer.Find(layers[0]) 188 | stage.SetEditTarget(Usd.EditTarget(layer)) 189 | stage.DefinePrim("/Foo") 190 | 191 | layer2 = Sdf.Layer.Find(layers[1]) 192 | stage.SetEditTarget(Usd.EditTarget(layer2)) 193 | stage.DefinePrim("/Bar") 194 | 195 | key = Tf.Notice.Register(unf.Notice.ObjectsChanged, _validate, stage) 196 | 197 | broker.BeginTransaction() 198 | 199 | stage.MuteLayer(layers[0]) 200 | stage.MuteLayer(layers[1]) 201 | stage.UnmuteLayer(layers[1]) 202 | stage.MuteAndUnmuteLayers([layers[2], layers[1]], []) 203 | 204 | broker.EndTransaction() 205 | 206 | # Ensure that one notice was received. 207 | assert len(received) == 1 208 | 209 | def test_mute_layers_transaction_layermutingchanged(stage_with_layers): 210 | """Mute several layers during transaction and analyze 211 | LayerMutingChanged notice. 212 | 213 | """ 214 | stage = stage_with_layers 215 | layers = stage.GetRootLayer().subLayerPaths 216 | 217 | broker = unf.Broker.Create(stage) 218 | 219 | received = [] 220 | 221 | def _validate(notice, stage): 222 | """Validate notice received.""" 223 | assert len(notice.GetMutedLayers()) == 3 224 | assert len(notice.GetUnmutedLayers()) == 0 225 | assert notice.GetMutedLayers()[0] == layers[0] 226 | assert notice.GetMutedLayers()[1] == layers[2] 227 | assert notice.GetMutedLayers()[2] == layers[1] 228 | received.append(notice) 229 | 230 | key = Tf.Notice.Register(unf.Notice.LayerMutingChanged, _validate, stage) 231 | 232 | broker.BeginTransaction() 233 | 234 | # Keep ref pointer to the layer we try to mute and unmute to 235 | # prevent it for being destroyed when it is muted. 236 | _layer = Sdf.Layer.FindOrOpen(layers[1]) 237 | 238 | stage.MuteLayer(layers[0]) 239 | stage.MuteLayer(layers[1]) 240 | stage.UnmuteLayer(layers[1]) 241 | stage.MuteAndUnmuteLayers([layers[2], layers[1]], []) 242 | 243 | broker.EndTransaction() 244 | 245 | # Ensure that one notice was received. 246 | assert len(received) == 1 247 | -------------------------------------------------------------------------------- /test/integration/testAddPrims.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | // namespace aliases for convenience. 15 | using _USD = PXR_NS::UsdNotice; 16 | namespace _UNF = unf::UnfNotice; 17 | 18 | class AddPrimsTest : public ::testing::Test { 19 | protected: 20 | using UsdListener = ::Test::Listener< 21 | _USD::StageNotice, _USD::StageContentsChanged, _USD::ObjectsChanged, 22 | _USD::StageEditTargetChanged, _USD::LayerMutingChanged>; 23 | 24 | using BrokerListener = ::Test::Listener< 25 | _UNF::StageNotice, _UNF::StageContentsChanged, _UNF::ObjectsChanged, 26 | _UNF::StageEditTargetChanged, _UNF::LayerMutingChanged>; 27 | 28 | void SetUp() override 29 | { 30 | _stage = PXR_NS::UsdStage::CreateInMemory(); 31 | _usdListener.SetStage(_stage); 32 | _brokerListener.SetStage(_stage); 33 | } 34 | 35 | PXR_NS::UsdStageRefPtr _stage; 36 | 37 | UsdListener _usdListener; 38 | BrokerListener _brokerListener; 39 | }; 40 | 41 | TEST_F(AddPrimsTest, Simple) 42 | { 43 | auto broker = unf::Broker::Create(_stage); 44 | 45 | _stage->DefinePrim(PXR_NS::SdfPath{"/Foo"}); 46 | _stage->DefinePrim(PXR_NS::SdfPath{"/Bar"}); 47 | _stage->DefinePrim(PXR_NS::SdfPath{"/Baz"}); 48 | 49 | // Ensure that similar notices are received via the stage and the broker. 50 | ASSERT_EQ(_usdListener.Received<_USD::StageNotice>(), 6); 51 | ASSERT_EQ(_usdListener.Received<_USD::StageContentsChanged>(), 3); 52 | ASSERT_EQ(_usdListener.Received<_USD::ObjectsChanged>(), 3); 53 | ASSERT_EQ(_usdListener.Received<_USD::StageEditTargetChanged>(), 0); 54 | ASSERT_EQ(_usdListener.Received<_USD::LayerMutingChanged>(), 0); 55 | 56 | ASSERT_EQ(_brokerListener.Received<_UNF::StageNotice>(), 6); 57 | ASSERT_EQ(_brokerListener.Received<_UNF::StageContentsChanged>(), 3); 58 | ASSERT_EQ(_brokerListener.Received<_UNF::ObjectsChanged>(), 3); 59 | ASSERT_EQ(_brokerListener.Received<_UNF::StageEditTargetChanged>(), 0); 60 | ASSERT_EQ(_brokerListener.Received<_UNF::LayerMutingChanged>(), 0); 61 | } 62 | 63 | TEST_F(AddPrimsTest, Batching) 64 | { 65 | auto broker = unf::Broker::Create(_stage); 66 | 67 | broker->BeginTransaction(); 68 | 69 | _stage->DefinePrim(PXR_NS::SdfPath{"/Foo"}); 70 | _stage->DefinePrim(PXR_NS::SdfPath{"/Bar"}); 71 | _stage->DefinePrim(PXR_NS::SdfPath{"/Baz"}); 72 | 73 | // Ensure that USD Notices are being sent as expected. 74 | ASSERT_EQ(_usdListener.Received<_USD::StageNotice>(), 6); 75 | ASSERT_EQ(_usdListener.Received<_USD::StageContentsChanged>(), 3); 76 | ASSERT_EQ(_usdListener.Received<_USD::ObjectsChanged>(), 3); 77 | ASSERT_EQ(_usdListener.Received<_USD::StageEditTargetChanged>(), 0); 78 | ASSERT_EQ(_usdListener.Received<_USD::LayerMutingChanged>(), 0); 79 | 80 | // While broker notices are blocked during a transaction. 81 | ASSERT_EQ(_brokerListener.Received<_UNF::StageNotice>(), 0); 82 | ASSERT_EQ(_brokerListener.Received<_UNF::StageContentsChanged>(), 0); 83 | ASSERT_EQ(_brokerListener.Received<_UNF::ObjectsChanged>(), 0); 84 | ASSERT_EQ(_brokerListener.Received<_UNF::StageEditTargetChanged>(), 0); 85 | ASSERT_EQ(_brokerListener.Received<_UNF::LayerMutingChanged>(), 0); 86 | 87 | broker->EndTransaction(); 88 | 89 | // Ensure that consolidated broker notices are sent after a transaction. 90 | ASSERT_EQ(_brokerListener.Received<_UNF::StageNotice>(), 2); 91 | ASSERT_EQ(_brokerListener.Received<_UNF::StageContentsChanged>(), 1); 92 | ASSERT_EQ(_brokerListener.Received<_UNF::ObjectsChanged>(), 1); 93 | ASSERT_EQ(_brokerListener.Received<_UNF::StageEditTargetChanged>(), 0); 94 | ASSERT_EQ(_brokerListener.Received<_UNF::LayerMutingChanged>(), 0); 95 | } 96 | 97 | TEST_F(AddPrimsTest, Blocking) 98 | { 99 | auto broker = unf::Broker::Create(_stage); 100 | 101 | // Pass a predicate to block all broker notices. 102 | broker->BeginTransaction([](const _UNF::StageNotice&) { return false; }); 103 | 104 | _stage->DefinePrim(PXR_NS::SdfPath{"/Foo"}); 105 | _stage->DefinePrim(PXR_NS::SdfPath{"/Bar"}); 106 | _stage->DefinePrim(PXR_NS::SdfPath{"/Baz"}); 107 | 108 | // Ensure that USD Notices are being sent as expected. 109 | ASSERT_EQ(_usdListener.Received<_USD::StageNotice>(), 6); 110 | ASSERT_EQ(_usdListener.Received<_USD::StageContentsChanged>(), 3); 111 | ASSERT_EQ(_usdListener.Received<_USD::ObjectsChanged>(), 3); 112 | ASSERT_EQ(_usdListener.Received<_USD::StageEditTargetChanged>(), 0); 113 | ASSERT_EQ(_usdListener.Received<_USD::LayerMutingChanged>(), 0); 114 | 115 | // While broker notices are blocked during a transaction. 116 | ASSERT_EQ(_brokerListener.Received<_UNF::StageNotice>(), 0); 117 | ASSERT_EQ(_brokerListener.Received<_UNF::StageContentsChanged>(), 0); 118 | ASSERT_EQ(_brokerListener.Received<_UNF::ObjectsChanged>(), 0); 119 | ASSERT_EQ(_brokerListener.Received<_UNF::StageEditTargetChanged>(), 0); 120 | ASSERT_EQ(_brokerListener.Received<_UNF::LayerMutingChanged>(), 0); 121 | 122 | broker->EndTransaction(); 123 | 124 | // Ensure that no broker notices are sent after a transaction either. 125 | ASSERT_EQ(_brokerListener.Received<_UNF::StageNotice>(), 0); 126 | ASSERT_EQ(_brokerListener.Received<_UNF::StageContentsChanged>(), 0); 127 | ASSERT_EQ(_brokerListener.Received<_UNF::ObjectsChanged>(), 0); 128 | ASSERT_EQ(_brokerListener.Received<_UNF::StageEditTargetChanged>(), 0); 129 | ASSERT_EQ(_brokerListener.Received<_UNF::LayerMutingChanged>(), 0); 130 | } 131 | 132 | TEST_F(AddPrimsTest, PartialBlocking) 133 | { 134 | auto broker = unf::Broker::Create(_stage); 135 | 136 | std::string target = typeid(_UNF::ObjectsChanged).name(); 137 | 138 | // Pass a predicate to block all broker notices. 139 | broker->BeginTransaction([&](const _UNF::StageNotice& n) { 140 | return (typeid(n).name() == target); 141 | }); 142 | 143 | _stage->DefinePrim(PXR_NS::SdfPath{"/Foo"}); 144 | _stage->DefinePrim(PXR_NS::SdfPath{"/Bar"}); 145 | _stage->DefinePrim(PXR_NS::SdfPath{"/Baz"}); 146 | 147 | // Ensure that USD Notices are being sent as expected. 148 | ASSERT_EQ(_usdListener.Received<_USD::StageNotice>(), 6); 149 | ASSERT_EQ(_usdListener.Received<_USD::StageContentsChanged>(), 3); 150 | ASSERT_EQ(_usdListener.Received<_USD::ObjectsChanged>(), 3); 151 | ASSERT_EQ(_usdListener.Received<_USD::StageEditTargetChanged>(), 0); 152 | ASSERT_EQ(_usdListener.Received<_USD::LayerMutingChanged>(), 0); 153 | 154 | // While broker notices are blocked during a transaction. 155 | ASSERT_EQ(_brokerListener.Received<_UNF::StageNotice>(), 0); 156 | ASSERT_EQ(_brokerListener.Received<_UNF::StageContentsChanged>(), 0); 157 | ASSERT_EQ(_brokerListener.Received<_UNF::ObjectsChanged>(), 0); 158 | ASSERT_EQ(_brokerListener.Received<_UNF::StageEditTargetChanged>(), 0); 159 | ASSERT_EQ(_brokerListener.Received<_UNF::LayerMutingChanged>(), 0); 160 | 161 | broker->EndTransaction(); 162 | 163 | // Ensure that only consolidated ObjectsChanged broker notice are sent. 164 | ASSERT_EQ(_brokerListener.Received<_UNF::StageNotice>(), 1); 165 | ASSERT_EQ(_brokerListener.Received<_UNF::StageContentsChanged>(), 0); 166 | ASSERT_EQ(_brokerListener.Received<_UNF::ObjectsChanged>(), 1); 167 | ASSERT_EQ(_brokerListener.Received<_UNF::StageEditTargetChanged>(), 0); 168 | ASSERT_EQ(_brokerListener.Received<_UNF::LayerMutingChanged>(), 0); 169 | } 170 | 171 | TEST_F(AddPrimsTest, Transaction_ObjectsChanged) 172 | { 173 | auto broker = unf::Broker::Create(_stage); 174 | 175 | ::Test::Observer<_UNF::ObjectsChanged> observer(_stage); 176 | 177 | broker->BeginTransaction(); 178 | 179 | _stage->DefinePrim(PXR_NS::SdfPath{"/Foo"}); 180 | _stage->DefinePrim(PXR_NS::SdfPath{"/Bar"}); 181 | _stage->DefinePrim(PXR_NS::SdfPath{"/Baz"}); 182 | 183 | ASSERT_EQ(observer.Received(), 0); 184 | 185 | broker->EndTransaction(); 186 | 187 | ASSERT_EQ(observer.Received(), 1); 188 | 189 | const auto& n = observer.GetLatestNotice(); 190 | ASSERT_EQ(n.GetResyncedPaths().size(), 3); 191 | ASSERT_EQ(n.GetResyncedPaths().at(0), PXR_NS::SdfPath{"/Bar"}); 192 | ASSERT_EQ(n.GetResyncedPaths().at(1), PXR_NS::SdfPath{"/Baz"}); 193 | ASSERT_EQ(n.GetResyncedPaths().at(2), PXR_NS::SdfPath{"/Foo"}); 194 | ASSERT_EQ(n.GetChangedInfoOnlyPaths().size(), 0); 195 | } 196 | --------------------------------------------------------------------------------