diff --git a/.github/workflows/main/installDependencies.py b/.github/workflows/main/installDependencies.py index ef04c7f7dad..dc1beb1677d 100755 --- a/.github/workflows/main/installDependencies.py +++ b/.github/workflows/main/installDependencies.py @@ -48,7 +48,7 @@ # Determine default archive URL. -defaultURL = "https://github.com/ImageEngine/cortex/releases/download/10.5.1.0/cortex-10.5.1.0-{platform}-python3.{extension}".format( +defaultURL = "https://github.com/ImageEngine/cortex/releases/download/10.5.2.0/cortex-10.5.2.0-{platform}-python3.{extension}".format( platform = { "darwin" : "osx", "win32" : "windows" }.get( sys.platform, "linux" ), extension = "tar.gz" if sys.platform != "win32" else "zip" ) diff --git a/Changes.md b/Changes.md index 3a6fe4d2749..fecb436c141 100644 --- a/Changes.md +++ b/Changes.md @@ -6,7 +6,11 @@ Breaking Changes - Dispatcher : Removed `createMatching()` method. -1.3.x.x (relative to 1.3.2.0) +1.3.x.x (relative to 1.3.3.0) +======= + + +1.3.3.0 (relative to 1.3.2.0) ======= Features @@ -21,6 +25,7 @@ Features Improvements ------------ +- SceneReader : Added support for reading from in-memory USD stages using a filename of the form `stageCache:{id}.usd` where `{id}` specifies a stage which has been inserted in the `UsdUtilsStageCache`. - Resample, Resize, Blur, ImageTransform : Improved performance, resulting in a 3x speedup in an obscure case, and a 5-10% speedup in more common cases. - ImageSampler : Added `interpolate` plug to control interpolation. Previously created ImageSamplers are unaffected, but interpolation is off by default for newly created ImageSamplers. - 3Delight : @@ -28,12 +33,22 @@ Improvements - Shaders (including light shaders) are only loaded from the `osl` subdirectory of the 3Delight installation. - Primitive variables named `uv` are now automatically renamed `st` for compatibility with the `uvCoord` shader's expectation. - Added a default `uvCoord` shader during internal shader network preprocessing to shader parameters that do not have an input connection. +- SetEditor : Added columns for controlling the Visible Set membership of set members. These allow the current members of a set to be included or excluded from the Visible Set by clicking within the Set Editor's Inclusions and Exclusions columns. Fixes ----- +- SceneReader : + - Fixed handling of invalid values on the following USD attributes : + - PointBased : `positions`, `normals`, `velocities`, `accelerations`. + - Curves : `widths`. + - PointInstancer : `ids`, `protoIndices`, `orientations`, `scales`, `velocities`, `accelerations`, `angularVelocities`. + - Points : `ids`, `widths`. + Invalid values are now ignored with a warning, instead of loading as invalid primitive variables. + - Fixed treatment of unconnected material outputs. If they were "authored" but not connected to a source, they were incorrectly being treated as valid attributes, and were being loaded as empty ShaderNetworks which caused problems elsewhere. - DispatchDialogue : Changed the button label for the results display from "Ok" to "Close". - Viewer : Fixed display of infinite values in the pixel inspectors. These were being incorrectly displayed as `nan` instead of `inf`. +- OptionTweaks : Fixed bug that prevented multiple tweaks being made to the same option in one node. API --- @@ -43,11 +58,13 @@ API - Added `findAllWithAttribute()` method, for finding all scene locations with a particular attribute. - ThreadState : Added `process()` method. - Process : Added const overload for `handleException()` method. The non-const version will be removed in future. +- ContextMonitor : Added `Statistics::variableHashes()` method, allowing introspection of specific variable values. Build ----- - MacOS : Fixed compilation with Clang 13. +- Cortex : Updated to version 10.5.2.0. 1.3.2.0 (relative to 1.3.1.0) ======= @@ -368,7 +385,11 @@ Build - USD : Updated to version 23.05. - ZLib : Added version 1.2.13. -1.2.10.x (relative to 1.2.10.2) +1.2.10.x (relative to 1.2.10.3) +======== + + +1.2.10.3 (relative to 1.2.10.2) ======== Fixes diff --git a/include/Gaffer/ContextMonitor.h b/include/Gaffer/ContextMonitor.h index 4aba28108cb..89aa4a1e711 100644 --- a/include/Gaffer/ContextMonitor.h +++ b/include/Gaffer/ContextMonitor.h @@ -73,9 +73,14 @@ class GAFFER_API ContextMonitor : public Monitor struct GAFFER_API Statistics { + using CountingMap = boost::unordered_map; + size_t numUniqueContexts() const; std::vector variableNames() const; size_t numUniqueValues( IECore::InternedString variableName ) const; + /// Maps from the `Context::variableHash()` for each unique value to + /// the number of times that value appeared. + const CountingMap &variableHashes( IECore::InternedString variableName ) const; Statistics & operator += ( const Context *rhs ); Statistics & operator += ( const Statistics &rhs ); @@ -86,7 +91,6 @@ class GAFFER_API ContextMonitor : public Monitor private : using ContextSet = boost::unordered_set; - using CountingMap = boost::unordered_map; using VariableMap = std::map; ContextSet m_contexts; diff --git a/python/GafferSceneTest/OptionTweaksTest.py b/python/GafferSceneTest/OptionTweaksTest.py index 2ae5a618fb2..bc117a2a5a2 100644 --- a/python/GafferSceneTest/OptionTweaksTest.py +++ b/python/GafferSceneTest/OptionTweaksTest.py @@ -139,6 +139,15 @@ def testCreateMode( self ) : self.assertEqual( tweaks["out"]["globals"].getValue()["option:test"], IECore.IntData( 10 ) ) + def testChainedTweaks( self ) : + + tweaks = GafferScene.OptionTweaks() + + tweaks["tweaks"].addChild( Gaffer.TweakPlug( "test", 1, Gaffer.TweakPlug.Mode.Create ) ) + tweaks["tweaks"].addChild( Gaffer.TweakPlug( "test", 10, Gaffer.TweakPlug.Mode.Multiply ) ) + tweaks["tweaks"].addChild( Gaffer.TweakPlug( "test", 2, Gaffer.TweakPlug.Mode.Add ) ) + + self.assertEqual( tweaks["out"].globals()["option:test"].value, 12 ) if __name__ == "__main__" : unittest.main() diff --git a/python/GafferSceneUI/SetEditor.py b/python/GafferSceneUI/SetEditor.py index 63195646e30..dbfebfa6d6f 100644 --- a/python/GafferSceneUI/SetEditor.py +++ b/python/GafferSceneUI/SetEditor.py @@ -71,11 +71,15 @@ def __init__( self, scriptNode, **kw ) : GafferUI.BasicPathFilterWidget( emptySetFilter ) self.__setMembersColumn = GafferUI.StandardPathColumn( "Members", "setPath:memberCount" ) + self.__includedSetMembersColumn = _GafferSceneUI._SetEditor.VisibleSetInclusionsColumn( scriptNode.context() ) + self.__excludedSetMembersColumn = _GafferSceneUI._SetEditor.VisibleSetExclusionsColumn( scriptNode.context() ) self.__pathListing = GafferUI.PathListingWidget( Gaffer.DictPath( {}, "/" ), # temp till we make a SetPath columns = [ _GafferSceneUI._SetEditor.SetNameColumn(), self.__setMembersColumn, + self.__includedSetMembersColumn, + self.__excludedSetMembersColumn, ], selectionMode = GafferUI.PathListingWidget.SelectionMode.Rows, displayMode = GafferUI.PathListingWidget.DisplayMode.Tree, @@ -164,23 +168,29 @@ def __selectedSetNames( self ) : def __dragBegin( self, widget, event ) : path = self.__pathListing.pathAt( imath.V2f( event.line.p0.x, event.line.p0.y ) ) - column = self.__pathListing.columnAt( imath.V2f( event.line.p0.x, event.line.p0.y ) ) selection = self.__pathListing.getSelection() - + setNames = [] if selection.match( str( path ) ) & IECore.PathMatcher.Result.ExactMatch : - if column == self.__setMembersColumn : - GafferUI.Pointer.setCurrent( "paths" ) - return IECore.StringVectorData( self.__getSetMembers().paths() ) - else : - selectedSetNames = self.__selectedSetNames() - if len( selectedSetNames ) > 0 : - GafferUI.Pointer.setCurrent( "paths" ) - return IECore.StringVectorData( selectedSetNames ) - else : - # prevent the path itself from being dragged - return IECore.StringVectorData() + setNames = self.__selectedSetNames() + else : + setName = path.property( "setPath:setName" ) + if setName is not None : + setNames.append( setName ) - return None + if len( setNames ) == 0 : + # prevent the path itself from being dragged + return IECore.StringVectorData() + + GafferUI.Pointer.setCurrent( "paths" ) + column = self.__pathListing.columnAt( imath.V2f( event.line.p0.x, event.line.p0.y ) ) + if column == self.__setMembersColumn : + return IECore.StringVectorData( self.__getSetMembers( setNames ).paths() ) + elif column == self.__includedSetMembersColumn : + return IECore.StringVectorData( self.__getIncludedSetMembers( setNames ).paths() ) + elif column == self.__excludedSetMembersColumn : + return IECore.StringVectorData( self.__getExcludedSetMembers( setNames ).paths() ) + else : + return IECore.StringVectorData( setNames ) def __keyPressSignal( self, widget, event ) : @@ -240,7 +250,7 @@ def __copySelectedSetNames( self, *unused ) : self.__plug.ancestor( Gaffer.ApplicationRoot ).setClipboardContents( data ) - def __getSetMembers( self, *unused ) : + def __getSetMembers( self, setNames, *unused ) : result = IECore.PathMatcher() @@ -248,21 +258,29 @@ def __getSetMembers( self, *unused ) : return result with Gaffer.Context( self.getContext() ) : - for setName in self.__selectedSetNames() : + for setName in setNames : result.addPaths( self.__plug.set( setName ).value ) return result + def __getIncludedSetMembers( self, setNames, *unused ) : + + return self.__getSetMembers( setNames ).intersection( ContextAlgo.getVisibleSet( self.getContext() ).inclusions ) + + def __getExcludedSetMembers( self, setNames, *unused ) : + + return self.__getSetMembers( setNames ).intersection( ContextAlgo.getVisibleSet( self.getContext() ).exclusions ) + def __selectSetMembers( self, *unused ) : - ContextAlgo.setSelectedPaths( self.getContext(), self.__getSetMembers() ) + ContextAlgo.setSelectedPaths( self.getContext(), self.__getSetMembers( self.__selectedSetNames() ) ) def __copySetMembers( self, *unused ) : data = IECore.StringVectorData() if self.__plug is not None : - data.extend( self.__getSetMembers().paths() ) + data.extend( self.__getSetMembers( self.__selectedSetNames() ).paths() ) self.__plug.ancestor( Gaffer.ApplicationRoot ).setClipboardContents( data ) diff --git a/python/GafferTest/ContextMonitorTest.py b/python/GafferTest/ContextMonitorTest.py index a1bcdae4ff1..71def872da6 100644 --- a/python/GafferTest/ContextMonitorTest.py +++ b/python/GafferTest/ContextMonitorTest.py @@ -107,5 +107,31 @@ def testRoot( self ) : self.assertFalse( a1["sum"] in m.allStatistics() ) self.assertTrue( a2["sum"] in m.allStatistics() ) + def testVariableHashes( self ) : + + node = GafferTest.AddNode() + + context1 = Gaffer.Context() + context1["test"] = 10 + + context2 = Gaffer.Context() + context2["test"] = 20 + + with Gaffer.ContextMonitor() as monitor : + + with context1 : + node["sum"].getValue() + + with context2 : + node["sum"].getValue() + + statistics = monitor.plugStatistics( node["sum"] ) + hashes = statistics.variableHashes( "test" ) + self.assertEqual( len( hashes ), 2 ) + self.assertEqual( hashes.get( context1.variableHash( "test" ) ), 2 ) # A hash and a compute + self.assertEqual( hashes.get( context2.variableHash( "test" ) ), 1 ) # Just a hash + + self.assertEqual( statistics.variableHashes( "nonExistentVariable" ), {} ) + if __name__ == "__main__": unittest.main() diff --git a/src/Gaffer/ContextMonitor.cpp b/src/Gaffer/ContextMonitor.cpp index f6faa902102..0fc062158ca 100644 --- a/src/Gaffer/ContextMonitor.cpp +++ b/src/Gaffer/ContextMonitor.cpp @@ -45,6 +45,7 @@ using namespace IECore; using namespace Gaffer; static ContextMonitor::Statistics g_emptyStatistics; +static ContextMonitor::Statistics::CountingMap g_emptyCountingMap; ////////////////////////////////////////////////////////////////////////// // ContextMonitor::Statistics @@ -75,6 +76,16 @@ size_t ContextMonitor::Statistics::numUniqueValues( IECore::InternedString varia return 0; } +const ContextMonitor::Statistics::CountingMap &ContextMonitor::Statistics::variableHashes( IECore::InternedString variableName ) const +{ + VariableMap::const_iterator it = m_variables.find( variableName ); + if( it != m_variables.end() ) + { + return it->second; + } + return g_emptyCountingMap; +} + ContextMonitor::Statistics & ContextMonitor::Statistics::operator += ( const Context *context ) { m_contexts.insert( context->hash() ); diff --git a/src/GafferModule/MonitorBinding.cpp b/src/GafferModule/MonitorBinding.cpp index 0182e9455d7..afd79fd1f5d 100644 --- a/src/GafferModule/MonitorBinding.cpp +++ b/src/GafferModule/MonitorBinding.cpp @@ -122,6 +122,16 @@ list contextMonitorVariableNames( const ContextMonitor::Statistics &s ) return result; } +dict contextMonitorVariableHashes( const ContextMonitor::Statistics &s, IECore::InternedString variableName ) +{ + dict result; + for( const auto &[hash, count] : s.variableHashes( variableName ) ) + { + result[hash] = count; + } + return result; +} + void annotateWrapper1( Node &root, const PerformanceMonitor &monitor, bool persistent ) { IECorePython::ScopedGILRelease gilRelease; @@ -299,6 +309,7 @@ void GafferModule::bindMonitor() .def( "numUniqueContexts", &ContextMonitor::Statistics::numUniqueContexts ) .def( "variableNames", &contextMonitorVariableNames ) .def( "numUniqueValues", &ContextMonitor::Statistics::numUniqueValues ) + .def( "variableHashes", &contextMonitorVariableHashes ) .def( self == self ) .def( self != self ) ; diff --git a/src/GafferScene/OptionTweaks.cpp b/src/GafferScene/OptionTweaks.cpp index 87993561811..8d76071cba3 100644 --- a/src/GafferScene/OptionTweaks.cpp +++ b/src/GafferScene/OptionTweaks.cpp @@ -120,12 +120,10 @@ IECore::ConstCompoundObjectPtr OptionTweaks::computeProcessedGlobals( CompoundObjectPtr result = new CompoundObject(); result->members() = inputGlobals->members(); - const CompoundObject *source = inputGlobals.get(); - tweaksPlug->applyTweaks( - [&source]( const std::string &valueName ) + [&result]( const std::string &valueName ) { - return source->member( g_namePrefix + valueName ); + return result->member( g_namePrefix + valueName ); }, [&result]( const std::string &valueName, DataPtr newData ) { diff --git a/src/GafferSceneUIModule/SetEditorBinding.cpp b/src/GafferSceneUIModule/SetEditorBinding.cpp index b1e70d21e9b..c3ae36e103a 100644 --- a/src/GafferSceneUIModule/SetEditorBinding.cpp +++ b/src/GafferSceneUIModule/SetEditorBinding.cpp @@ -407,6 +407,433 @@ class SetNameColumn : public StandardPathColumn }; +////////////////////////////////////////////////////////////////////////// +// VisibleSetInclusionsColumn - displays and modifies inclusions membership +// of the VisibleSet in the provided context. +////////////////////////////////////////////////////////////////////////// + +class VisibleSetInclusionsColumn : public PathColumn +{ + + public : + + IE_CORE_DECLAREMEMBERPTR( VisibleSetInclusionsColumn ) + + VisibleSetInclusionsColumn( ContextPtr context ) + : PathColumn(), m_context( context ) + { + buttonPressSignal().connect( boost::bind( &VisibleSetInclusionsColumn::buttonPress, this, ::_3 ) ); + buttonReleaseSignal().connect( boost::bind( &VisibleSetInclusionsColumn::buttonRelease, this, ::_1, ::_2, ::_3 ) ); + m_context->changedSignal().connect( boost::bind( &VisibleSetInclusionsColumn::contextChanged, this, ::_2 ) ); + } + + CellData cellData( const Gaffer::Path &path, const IECore::Canceller *canceller ) const override + { + CellData result; + + auto setPath = IECore::runTimeCast( &path ); + if( !setPath ) + { + return result; + } + + const auto setName = runTimeCast( setPath->property( g_setNamePropertyName ) ); + if( !setName ) + { + // We only interact with locations representing sets + return result; + } + + auto iconData = new CompoundData; + iconData->writable()["state:highlighted"] = g_setIncludedHighlightedTransparentIconName; + result.icon = iconData; + result.toolTip = g_inclusionToolTip; + + const auto visibleSet = ContextAlgo::getVisibleSet( m_context.get() ); + if( visibleSet.inclusions.isEmpty() ) + { + result.value = new IntData( 0 ); + return result; + } + + Context::Scope scopedContext( m_context.get() ); + const auto setMembers = setPath->getScene()->set( setName->readable() ); + const auto includedSetMembers = setMembers->readable().intersection( visibleSet.inclusions ); + result.value = new IntData( includedSetMembers.size() ); + if( includedSetMembers.isEmpty() ) + { + return result; + } + + size_t excludedSetMemberCount = 0; + if( !visibleSet.exclusions.isEmpty() ) + { + for( IECore::PathMatcher::Iterator it = includedSetMembers.begin(), eIt = includedSetMembers.end(); it != eIt; ++it ) + { + const auto visibility = visibleSet.visibility( *it ); + if( visibility.drawMode != GafferScene::VisibleSet::Visibility::Visible ) + { + excludedSetMemberCount++; + } + } + } + + iconData->writable()["state:highlighted"] = g_setIncludedHighlightedIconName; + const bool allSetMembersIncluded = includedSetMembers.size() == setMembers->readable().size(); + if( excludedSetMemberCount == 0 ) + { + iconData->writable()["state:normal"] = allSetMembersIncluded ? g_setIncludedIconName : g_setPartiallyIncludedIconName; + result.toolTip = allSetMembersIncluded ? g_setIncludedToolTip : g_setPartiallyIncludedToolTip; + } + else if( includedSetMembers.size() == excludedSetMemberCount ) + { + iconData->writable()["state:normal"] = g_setIncludedDisabledIconName; + result.toolTip = allSetMembersIncluded ? g_setIncludedOverrideToolTip : g_setPartiallyIncludedOverrideToolTip; + } + else + { + iconData->writable()["state:normal"] = g_setPartiallyDisabledIconName; + result.toolTip = allSetMembersIncluded ? g_setIncludedPartialOverrideToolTip : g_setPartiallyIncludedPartialOverrideToolTip; + } + + return result; + } + + CellData headerData( const IECore::Canceller *canceller ) const override + { + const auto visibleSet = ContextAlgo::getVisibleSet( m_context.get() ); + return CellData( /* value = */ nullptr, /* icon = */ visibleSet.inclusions.isEmpty() ? g_inclusionsEmptyIconName : g_setIncludedIconName, /* background = */ nullptr, /* tooltip = */ new StringData( "Visible Set Inclusions" ) ); + } + + private : + + void contextChanged( const IECore::InternedString &name ) + { + if( ContextAlgo::affectsVisibleSet( name ) ) + { + changedSignal()( this ); + } + } + + bool buttonPress( const ButtonEvent &event ) + { + if( event.buttons != ButtonEvent::Left ) + { + return false; + } + + return true; + } + + bool buttonRelease( const Gaffer::Path &path, const GafferUI::PathListingWidget &widget, const ButtonEvent &event ) + { + auto setPath = IECore::runTimeCast( &path ); + if( !setPath ) + { + return false; + } + + const auto setName = runTimeCast( setPath->property( g_setNamePropertyName ) ); + if( !setName ) + { + // We only interact with locations representing sets + return false; + } + + Context::Scope scopedContext( m_context.get() ); + const auto setMembers = setPath->getScene()->set( setName->readable() ); + auto pathsToInclude = IECore::PathMatcher( setMembers->readable() ); + const auto selection = widget.getSelection(); + if( std::holds_alternative( selection ) ) + { + // Permit bulk editing of a selection of set names when clicking on one of the selected set names + const auto selectedPaths = std::get( selection ); + if( selectedPaths.match( setPath->names() ) & IECore::PathMatcher::Result::ExactMatch ) + { + auto selectedSetPath = setPath->copy(); + for( IECore::PathMatcher::Iterator it = selectedPaths.begin(), eIt = selectedPaths.end(); it != eIt; ++it ) + { + selectedSetPath->setFromString( ScenePlug::pathToString( *it ) ); + const auto selectedSetName = runTimeCast( selectedSetPath->property( g_setNamePropertyName ) ); + if( selectedSetName && selectedSetName->readable() != setName->readable() ) + { + pathsToInclude.addPaths( setPath->getScene()->set( selectedSetName->readable() )->readable() ); + } + } + } + } + + bool update = false; + auto visibleSet = ContextAlgo::getVisibleSet( m_context.get() ); + if( event.button == ButtonEvent::Left && !event.modifiers ) + { + const auto includedSetMembers = setMembers->readable().intersection( visibleSet.inclusions ); + if( includedSetMembers.isEmpty() ) + { + update = visibleSet.inclusions.addPaths( pathsToInclude ); + } + else + { + update = visibleSet.inclusions.removePaths( pathsToInclude ); + } + } + else if( event.button == ButtonEvent::Left && event.modifiers == ButtonEvent::Modifiers::Shift ) + { + update = visibleSet.inclusions.addPaths( pathsToInclude ); + } + + if( update ) + { + ContextAlgo::setVisibleSet( m_context.get(), visibleSet ); + } + + return true; + } + + ContextPtr m_context; + + static IECore::StringDataPtr g_setIncludedIconName; + static IECore::StringDataPtr g_setIncludedDisabledIconName; + static IECore::StringDataPtr g_setIncludedHighlightedIconName; + static IECore::StringDataPtr g_setIncludedHighlightedTransparentIconName; + static IECore::StringDataPtr g_setPartiallyIncludedIconName; + static IECore::StringDataPtr g_setPartiallyDisabledIconName; + static IECore::StringDataPtr g_inclusionsEmptyIconName; + + static IECore::StringDataPtr g_inclusionToolTip; + static IECore::StringDataPtr g_setIncludedToolTip; + static IECore::StringDataPtr g_setIncludedOverrideToolTip; + static IECore::StringDataPtr g_setIncludedPartialOverrideToolTip; + static IECore::StringDataPtr g_setPartiallyIncludedToolTip; + static IECore::StringDataPtr g_setPartiallyIncludedOverrideToolTip; + static IECore::StringDataPtr g_setPartiallyIncludedPartialOverrideToolTip; + +}; + +StringDataPtr VisibleSetInclusionsColumn::g_setIncludedIconName = new StringData( "locationIncluded.png" ); +StringDataPtr VisibleSetInclusionsColumn::g_setIncludedDisabledIconName = new StringData( "locationIncludedDisabled.png" ); +StringDataPtr VisibleSetInclusionsColumn::g_setIncludedHighlightedIconName = new StringData( "locationIncludedHighlighted.png" ); +StringDataPtr VisibleSetInclusionsColumn::g_setIncludedHighlightedTransparentIconName = new StringData( "locationIncludedHighlightedTransparent.png" ); +StringDataPtr VisibleSetInclusionsColumn::g_setPartiallyIncludedIconName = new StringData( "descendantIncluded.png" ); +StringDataPtr VisibleSetInclusionsColumn::g_setPartiallyDisabledIconName = new StringData( "descendantIncludedTransparent.png" ); +StringDataPtr VisibleSetInclusionsColumn::g_inclusionsEmptyIconName = new StringData( "locationIncludedTransparent.png" ); + +StringDataPtr VisibleSetInclusionsColumn::g_inclusionToolTip = new StringData( "Click to include the current members of this set in the Visible Set, causing them to always appear in Viewers." ); +StringDataPtr VisibleSetInclusionsColumn::g_setIncludedToolTip = new StringData( + "All members are in the Visible Set, causing them to always appear in Viewers.\n\n" + "Click to remove members from the Visible Set." +); +StringDataPtr VisibleSetInclusionsColumn::g_setIncludedOverrideToolTip = new StringData( + "All members are in the Visible Set, but aren't visible due to being overridden by an exclusion.\n\n" + "Click to remove members from the Visible Set." +); +StringDataPtr VisibleSetInclusionsColumn::g_setIncludedPartialOverrideToolTip = new StringData( + "All members are in the Visible Set, but some aren't visible due to being overridden by an exclusion.\n\n" + "Click to remove members from the Visible Set." +); +StringDataPtr VisibleSetInclusionsColumn::g_setPartiallyIncludedToolTip = new StringData( + "Some members are in the Visible Set, causing them to always appear in Viewers.\n\n" + "Click to remove members from the Visible Set.\n" + "Shift-click to include members in the Visible Set." +); +StringDataPtr VisibleSetInclusionsColumn::g_setPartiallyIncludedOverrideToolTip = new StringData( + "Some members are in the Visible Set, but aren't visible due to being overridden by an exclusion.\n\n" + "Click to remove members from the Visible Set.\n" + "Shift-click to include members in the Visible Set." +); +StringDataPtr VisibleSetInclusionsColumn::g_setPartiallyIncludedPartialOverrideToolTip = new StringData( + "Some members are in the Visible Set, but some aren't visible due to being overridden by an exclusion.\n\n" + "Click to remove members from the Visible Set.\n" + "Shift-click to include members in the Visible Set." +); + +////////////////////////////////////////////////////////////////////////// +// VisibleSetExclusionsColumn - displays and modifies exclusions membership +// of the VisibleSet in the provided context. +////////////////////////////////////////////////////////////////////////// + +class VisibleSetExclusionsColumn : public PathColumn +{ + + public : + + IE_CORE_DECLAREMEMBERPTR( VisibleSetExclusionsColumn ) + + VisibleSetExclusionsColumn( ContextPtr context ) + : PathColumn(), m_context( context ) + { + buttonPressSignal().connect( boost::bind( &VisibleSetExclusionsColumn::buttonPress, this, ::_3 ) ); + buttonReleaseSignal().connect( boost::bind( &VisibleSetExclusionsColumn::buttonRelease, this, ::_1, ::_2, ::_3 ) ); + m_context->changedSignal().connect( boost::bind( &VisibleSetExclusionsColumn::contextChanged, this, ::_2 ) ); + } + + CellData cellData( const Gaffer::Path &path, const IECore::Canceller *canceller ) const override + { + CellData result; + + auto setPath = IECore::runTimeCast( &path ); + if( !setPath ) + { + return result; + } + + const auto setName = runTimeCast( setPath->property( g_setNamePropertyName ) ); + if( !setName ) + { + // We only interact with locations representing sets + return result; + } + + auto iconData = new CompoundData; + iconData->writable()["state:highlighted"] = g_setExcludedHighlightedTransparentIconName; + result.icon = iconData; + result.toolTip = g_exclusionToolTip; + + const auto visibleSet = ContextAlgo::getVisibleSet( m_context.get() ); + if( visibleSet.exclusions.isEmpty() ) + { + result.value = new IntData( 0 ); + return result; + } + + Context::Scope scopedContext( m_context.get() ); + const auto setMembers = setPath->getScene()->set( setName->readable() ); + const auto excludedSetMembers = setMembers->readable().intersection( visibleSet.exclusions ); + result.value = new IntData( excludedSetMembers.size() ); + if( excludedSetMembers.isEmpty() ) + { + return result; + } + + const bool allSetMembersExcluded = excludedSetMembers.size() == setMembers->readable().size(); + iconData->writable()["state:highlighted"] = g_setExcludedHighlightedIconName; + iconData->writable()["state:normal"] = allSetMembersExcluded ? g_setExcludedIconName : g_setPartiallyExcludedIconName; + result.toolTip = allSetMembersExcluded ? g_setExcludedToolTip : g_setPartiallyExcludedToolTip; + + return result; + } + + CellData headerData( const IECore::Canceller *canceller ) const override + { + const auto visibleSet = ContextAlgo::getVisibleSet( m_context.get() ); + return CellData( /* value = */ nullptr, /* icon = */ visibleSet.exclusions.isEmpty() ? g_exclusionsEmptyIconName : g_setExcludedIconName, /* background = */ nullptr, /* tooltip = */ new StringData( "Visible Set Exclusions" ) ); + } + + private : + + void contextChanged( const IECore::InternedString &name ) + { + if( ContextAlgo::affectsVisibleSet( name ) ) + { + changedSignal()( this ); + } + } + + bool buttonPress( const ButtonEvent &event ) + { + if( event.buttons != ButtonEvent::Left ) + { + return false; + } + + return true; + } + + bool buttonRelease( const Gaffer::Path &path, const GafferUI::PathListingWidget &widget, const ButtonEvent &event ) + { + auto setPath = IECore::runTimeCast( &path ); + if( !setPath ) + { + return false; + } + + const auto setName = runTimeCast( setPath->property( g_setNamePropertyName ) ); + if( !setName ) + { + // We only interact with locations representing sets + return false; + } + + Context::Scope scopedContext( m_context.get() ); + const auto setMembers = setPath->getScene()->set( setName->readable() ); + auto pathsToExclude = IECore::PathMatcher( setMembers->readable() ); + const auto selection = widget.getSelection(); + if( std::holds_alternative( selection ) ) + { + // Permit bulk editing of a selection of set names when clicking on one of the selected set names + const auto selectedPaths = std::get( selection ); + if( selectedPaths.match( setPath->names() ) & IECore::PathMatcher::Result::ExactMatch ) + { + auto selectedSetPath = setPath->copy(); + for( IECore::PathMatcher::Iterator it = selectedPaths.begin(), eIt = selectedPaths.end(); it != eIt; ++it ) + { + selectedSetPath->setFromString( ScenePlug::pathToString( *it ) ); + const auto selectedSetName = runTimeCast( selectedSetPath->property( g_setNamePropertyName ) ); + if( selectedSetName && selectedSetName->readable() != setName->readable() ) + { + pathsToExclude.addPaths( setPath->getScene()->set( selectedSetName->readable() )->readable() ); + } + } + } + } + + bool update = false; + auto visibleSet = ContextAlgo::getVisibleSet( m_context.get() ); + if( event.button == ButtonEvent::Left && !event.modifiers ) + { + const auto excludedSetMembers = setMembers->readable().intersection( visibleSet.exclusions ); + if( excludedSetMembers.isEmpty() ) + { + update = visibleSet.exclusions.addPaths( pathsToExclude ); + } + else + { + update = visibleSet.exclusions.removePaths( pathsToExclude ); + } + } + else if( event.button == ButtonEvent::Left && event.modifiers == ButtonEvent::Modifiers::Shift ) + { + update = visibleSet.exclusions.addPaths( pathsToExclude ); + } + + if( update ) + { + ContextAlgo::setVisibleSet( m_context.get(), visibleSet ); + } + + return true; + } + + ContextPtr m_context; + + static IECore::StringDataPtr g_setExcludedIconName; + static IECore::StringDataPtr g_setPartiallyExcludedIconName; + static IECore::StringDataPtr g_setExcludedHighlightedIconName; + static IECore::StringDataPtr g_setExcludedHighlightedTransparentIconName; + static IECore::StringDataPtr g_exclusionsEmptyIconName; + + static IECore::StringDataPtr g_exclusionToolTip; + static IECore::StringDataPtr g_setExcludedToolTip; + static IECore::StringDataPtr g_setPartiallyExcludedToolTip; +}; + +StringDataPtr VisibleSetExclusionsColumn::g_setExcludedIconName = new StringData( "locationExcluded.png" ); +StringDataPtr VisibleSetExclusionsColumn::g_setPartiallyExcludedIconName = new StringData( "descendantExcluded.png" ); +StringDataPtr VisibleSetExclusionsColumn::g_setExcludedHighlightedIconName = new StringData( "locationExcludedHighlighted.png" ); +StringDataPtr VisibleSetExclusionsColumn::g_setExcludedHighlightedTransparentIconName = new StringData( "locationExcludedHighlightedTransparent.png" ); +StringDataPtr VisibleSetExclusionsColumn::g_exclusionsEmptyIconName = new StringData( "locationExcludedTransparent.png" ); + +StringDataPtr VisibleSetExclusionsColumn::g_exclusionToolTip = new StringData( "Click to exclude the current members of this set from the Visible Set, causing them to not appear in Viewers." ); +StringDataPtr VisibleSetExclusionsColumn::g_setExcludedToolTip = new StringData( + "All members are excluded from the Visible Set, causing them to not appear in Viewers.\n\n" + "Click to remove the exclusion." +); +StringDataPtr VisibleSetExclusionsColumn::g_setPartiallyExcludedToolTip = new StringData( + "Some members are excluded from the Visible Set, causing them to not appear in Viewers.\n\n" + "Click to remove the exclusion.\n" + "Shift-click to exclude members from the Visible Set." +); + ////////////////////////////////////////////////////////////////////////// // SetEditorSearchFilter - filters based on a match pattern. This // removes non-leaf paths if all their children have also been @@ -604,4 +1031,12 @@ void GafferSceneUIModule::bindSetEditor() .def( init<>() ) ; + RefCountedClass( "VisibleSetInclusionsColumn" ) + .def( init< ContextPtr >() ) + ; + + RefCountedClass( "VisibleSetExclusionsColumn" ) + .def( init< ContextPtr >() ) + ; + }