diff --git a/Changes.md b/Changes.md index 349e01a660..1e69b4d8f9 100644 --- a/Changes.md +++ b/Changes.md @@ -9,14 +9,26 @@ Breaking Changes 1.5.x.x (relative to 1.5.3.0) ======= +Improvements +------------ + +- AttributeEditor : Added "Select Affected Objects" menu item to the "Linked Lights" and Arnold "Shadow Group" columns. + Fixes ----- +- AttributeEditor : Fixed display of fallback value for `linkedLights` attribute. - AttributeEditor, LightEditor, RenderPassEditor : - Fixed bugs which prevented edits being made in "Source" scope when there was a downstream edit in an EditScope (#6172). - Fixed warning messages when attempting to disable a non-existent edit. - Fixed warning message which referred to "None" rather than the "Source" scope. +API +--- + +- RenderPassEditor : Added optional `index` argument to `registerOption()` and `registerColumn()`. This can be used to specify the column's position in the UI. +- Metadata : Added `targetsWithMetadata()` function, returning all the string targets which match a pattern and have a specific metadata key. + 1.5.3.0 (relative to 1.5.2.0) ======= diff --git a/include/Gaffer/Metadata.h b/include/Gaffer/Metadata.h index 4aed04039f..b89e661304 100644 --- a/include/Gaffer/Metadata.h +++ b/include/Gaffer/Metadata.h @@ -139,6 +139,10 @@ class GAFFER_API Metadata /// Utilities /// ========= + /// Returns the names of all matching string targets with the specified + /// metadata key. + static std::vector targetsWithMetadata( const IECore::StringAlgo::MatchPattern &targetPattern, IECore::InternedString key ); + /// Lists all node descendants of "root" with the specified metadata key. /// If instanceOnly is true the search is restricted to instance metadata. static std::vector nodesWithMetadata( GraphComponent *root, IECore::InternedString key, bool instanceOnly = false ); diff --git a/python/GafferSceneUI/RenderPassEditor.py b/python/GafferSceneUI/RenderPassEditor.py index d96335acc7..d3a8c91900 100644 --- a/python/GafferSceneUI/RenderPassEditor.py +++ b/python/GafferSceneUI/RenderPassEditor.py @@ -153,7 +153,7 @@ def __init__( self, scriptNode, **kw ) : __columnRegistry = collections.OrderedDict() @classmethod - def registerOption( cls, groupKey, optionName, section = "Main", columnName = None ) : + def registerOption( cls, groupKey, optionName, section = "Main", columnName = None, index = None ) : optionLabel = Gaffer.Metadata.value( "option:" + optionName, "label" ) if not columnName : @@ -173,7 +173,8 @@ def registerOption( cls, groupKey, optionName, section = "Main", columnName = No columnName, toolTip ), - section + section, + index ) # Registers a column in the Render Pass Editor. @@ -181,12 +182,12 @@ def registerOption( cls, groupKey, optionName, section = "Main", columnName = No # `inspectorFunction( scene, editScope )` returning a # `GafferSceneUI.Private.InspectorColumn` object. @classmethod - def registerColumn( cls, groupKey, columnKey, inspectorFunction, section = "Main" ) : + def registerColumn( cls, groupKey, columnKey, inspectorFunction, section = "Main", index = None ) : sections = cls.__columnRegistry.setdefault( groupKey, collections.OrderedDict() ) section = sections.setdefault( section, collections.OrderedDict() ) - section[columnKey] = inspectorFunction + section[columnKey] = ( inspectorFunction, index ) @classmethod def deregisterColumn( cls, groupKey, columnKey, section = "Main" ) : @@ -287,9 +288,26 @@ def __updateColumns( self ) : for groupKey, sections in self.__columnRegistry.items() : if IECore.StringAlgo.match( tabGroup, groupKey ) : section = sections.get( currentSection or None, {} ) - sectionColumns += [ c( self.settings()["in"], self.settings()["editScope"] ) for c in section.values() ] + sectionColumns += [ ( c( self.settings()["in"], self.settings()["editScope"] ), index ) for ( c, index ) in section.values() ] - self.__pathListing.setColumns( [ self.__renderPassNameColumn, self.__renderPassActiveColumn ] + sectionColumns ) + self.__pathListing.setColumns( [ self.__renderPassNameColumn, self.__renderPassActiveColumn ] + self.__orderedColumns( sectionColumns ) ) + + @staticmethod + def __orderedColumns( columnsAndIndices ) : + + for i, ( column, index ) in enumerate( columnsAndIndices ) : + if index is not None : + # Negative indices are remapped to their absolute position in the column list. + columnsAndIndices[i] = ( column, index if index >= 0 else len( columnsAndIndices ) + index ) + + # As column indices may be sparse, we fill in the gaps with any unspecified indices before sorting. + availableIndices = iter( sorted( set( range( len( columnsAndIndices ) ) ) - { x[1] for x in columnsAndIndices } ) ) + orderedColumns = sorted( + [ ( column, index if index is not None else next( availableIndices ) ) for column, index in columnsAndIndices ], + key = lambda x: x[1] + ) + + return [ x[0] for x in orderedColumns ] @GafferUI.LazyMethod( deferUntilPlaybackStops = True ) def __setPathListingPath( self ) : diff --git a/python/GafferSceneUITest/RenderPassEditorTest.py b/python/GafferSceneUITest/RenderPassEditorTest.py index 83c7e1d97e..365dfed168 100644 --- a/python/GafferSceneUITest/RenderPassEditorTest.py +++ b/python/GafferSceneUITest/RenderPassEditorTest.py @@ -434,3 +434,112 @@ def testFn( name ) : self.assertEqual( [ str( c ) for c in path.children() ], [ "/A/A_A", "/A/A_B" ] ) path.setFromString( "/B" ) self.assertEqual( [ str( c ) for c in path.children() ], [ "/B/B_C", "/B/B_D" ] ) + + def testRegisterOption( self ) : + + for columnName in [ "A", "B", "C", "D", "E" ] : + GafferSceneUI.RenderPassEditor.registerOption( "*", columnName, "Test" ) + self.addCleanup( GafferSceneUI.RenderPassEditor.deregisterColumn, "*", columnName, "Test" ) + + script = Gaffer.ScriptNode() + + editor = GafferSceneUI.RenderPassEditor( script ) + editor.settings()["section"].setValue( "Test" ) + + GafferSceneUI.RenderPassEditor._RenderPassEditor__updateColumns.flush( editor ) + + pathListing = editor._RenderPassEditor__pathListing + + columnNames = [ c.headerData().value for c in pathListing.getColumns() ] + for columnName in [ "A", "B", "C", "D", "E" ] : + self.assertIn( columnName, columnNames ) + + GafferSceneUI.RenderPassEditor.deregisterColumn( "*", "B", "Test" ) + + editor._RenderPassEditor__updateColumns() + GafferSceneUI.RenderPassEditor._RenderPassEditor__updateColumns.flush( editor ) + + columnNames = [ c.headerData().value for c in pathListing.getColumns() ] + self.assertNotIn( "B", columnNames ) + self.assertIn( "A", columnNames ) + self.assertIn( "C", columnNames ) + self.assertIn( "D", columnNames ) + self.assertIn( "E", columnNames ) + + def testColumnOrder( self ) : + + script = Gaffer.ScriptNode() + editor = GafferSceneUI.RenderPassEditor( script ) + editor.settings()["section"].setValue( "Test" ) + + def assertColumnOrder( columns, order ) : + + for columnName, index in columns : + GafferSceneUI.RenderPassEditor.registerOption( "*", columnName, "Test", index = index ) + self.addCleanup( GafferSceneUI.RenderPassEditor.deregisterColumn, "*", columnName, "Test" ) + + editor._RenderPassEditor__updateColumns() + GafferSceneUI.RenderPassEditor._RenderPassEditor__updateColumns.flush( editor ) + columnNames = [ c.headerData().value for c in editor._RenderPassEditor__pathListing.getColumns() if isinstance( c, GafferSceneUI.Private.InspectorColumn ) ] + self.assertEqual( columnNames, order ) + + for columnName, _ in columns : + GafferSceneUI.RenderPassEditor.deregisterColumn( "*", columnName, "Test" ) + + assertColumnOrder( + [ ( "A", None ), ( "B", None ), ( "C", None ), ( "D", None ), ( "E", None ) ], + [ "A", "B", "C", "D", "E" ] + ) + + assertColumnOrder( + [ ( "E", None ), ( "D", None ), ( "C", None ), ( "B", None ), ( "A", None ) ], + [ "E", "D", "C", "B", "A" ] + ) + + assertColumnOrder( + [ ( "A", 0 ), ( "B", 1 ), ( "C", 2 ), ( "D", 3 ), ( "E", 4 ) ], + [ "A", "B", "C", "D", "E" ] + ) + + assertColumnOrder( + [ ( "A", 4 ), ( "B", 3 ), ( "C", 2 ), ( "D", 1 ), ( "E", 0 ) ], + [ "E", "D", "C", "B", "A" ] + ) + + assertColumnOrder( + [ ( "A", None ), ( "B", None ), ( "C", None ), ( "D", 0 ), ( "E", 2 ) ], + [ "D", "A", "E", "B", "C" ] + ) + + assertColumnOrder( + [ ( "A", None ), ( "B", None ), ( "C", 0 ), ( "D", -1 ), ( "E", None ) ], + [ "C", "A", "B", "E", "D" ] + ) + + assertColumnOrder( + [ ( "A", None ), ( "B", None ), ( "C", -1 ), ( "D", -2 ), ( "E", None ) ], + [ "A", "B", "E", "D", "C" ] + ) + + assertColumnOrder( + [ ( "A", None ), ( "B", -1 ), ( "C", None ), ( "D", -1 ), ( "E", None ) ], + [ "A", "C", "E", "B", "D" ] + ) + + assertColumnOrder( + [ ( "A", None ), ( "B", 0 ), ( "C", 0 ), ( "D", None ), ( "E", 2 ) ], + [ "B", "C", "A", "E", "D" ] + ) + + assertColumnOrder( + [ ( "A", None ), ( "B", 0 ), ( "C", 0 ), ( "D", 0 ), ( "E", 2 ) ], + [ "B", "C", "D", "A", "E" ] + ) + + assertColumnOrder( + [ ( "A", None ), ( "B", 0 ), ( "C", 0 ), ( "D", 0 ), ( "E", 1 ) ], + [ "B", "C", "D", "E", "A" ] + ) + +if __name__ == "__main__" : + unittest.main() diff --git a/python/GafferTest/MetadataTest.py b/python/GafferTest/MetadataTest.py index fba20a107f..c13162a9f2 100644 --- a/python/GafferTest/MetadataTest.py +++ b/python/GafferTest/MetadataTest.py @@ -1356,6 +1356,23 @@ def changed( node, key, reason ) : Gaffer.Metadata.registerValue( node, "test", 2 ) self.assertEqual( Gaffer.Metadata.value( node, "test" ), 1 ) + def testTargetsWithMetadata( self ) : + + for target, key in [ + [ "target1", "k1" ], + [ "target1", "k2" ], + [ "target2", "k2" ], + [ "target2", "k3" ], + [ "target3", "k4" ], + [ "targetA", "k1" ], + ] : + Gaffer.Metadata.registerValue( target, key, "test" ) + self.addCleanup( Gaffer.Metadata.deregisterValue, target, key ) + + self.assertEqual( Gaffer.Metadata.targetsWithMetadata( "target[1-3]", "k2" ), [ "target1", "target2" ] ) + self.assertEqual( Gaffer.Metadata.targetsWithMetadata( "target*", "k1" ), [ "target1", "targetA" ] ) + self.assertEqual( Gaffer.Metadata.targetsWithMetadata( "*", "k3" ), [ "target2" ] ) + def tearDown( self ) : GafferTest.TestCase.tearDown( self ) diff --git a/src/Gaffer/Metadata.cpp b/src/Gaffer/Metadata.cpp index fdd768b352..1b070aea4e 100644 --- a/src/Gaffer/Metadata.cpp +++ b/src/Gaffer/Metadata.cpp @@ -217,7 +217,17 @@ using Values = multi_index::multi_index_container< > >; -using MetadataMap = std::map; +using NamedValues = std::pair; + +using MetadataMap = multi_index::multi_index_container< + NamedValues, + multi_index::indexed_by< + multi_index::ordered_unique< + multi_index::member + >, + multi_index::sequenced<> + > +>; MetadataMap &metadataMap() { @@ -481,12 +491,23 @@ void Metadata::registerValue( IECore::InternedString target, IECore::InternedStr void Metadata::registerValue( IECore::InternedString target, IECore::InternedString key, ValueFunction value ) { - NamedValue namedValue( key, value ); - auto &m = metadataMap()[target]; - auto i = m.insert( namedValue ); - if( !i.second ) + auto &targetMap = metadataMap(); + + auto targetIt = targetMap.find( target ); + if( targetIt == targetMap.end() ) + { + targetIt = targetMap.insert( NamedValues( target, Values() ) ).first; + } + + // Cast is safe because we don't use `second` as a key in the `multi_index_container`, + // so we can modify it without affecting indexing. + Values &values = const_cast( targetIt->second ); + + const NamedValue namedValue( key, value ); + auto keyIt = values.insert( namedValue ); + if( !keyIt.second ) { - m.replace( i.first, namedValue ); + values.replace( keyIt.first, namedValue ); } valueChangedSignal()( target, key ); @@ -501,13 +522,17 @@ void Metadata::deregisterValue( IECore::InternedString target, IECore::InternedS return; } - auto vIt = mIt->second.find( key ); - if( vIt == mIt->second.end() ) + // Cast is safe because we don't use `second` as a key in the `multi_index_container`, + // so we can modify it without affecting indexing. + Values &values = const_cast( mIt->second ); + + auto vIt = values.find( key ); + if( vIt == values.end() ) { return; } - mIt->second.erase( vIt ); + values.erase( vIt ); valueChangedSignal()( target, key ); } @@ -544,6 +569,25 @@ IECore::ConstDataPtr Metadata::valueInternal( IECore::InternedString target, IEC return nullptr; } +std::vector Metadata::targetsWithMetadata( const IECore::StringAlgo::MatchPattern &targetPattern, IECore::InternedString key ) +{ + vector result; + const auto &orderedIndex = metadataMap().get<1>(); + for( const auto &[target, values] : orderedIndex ) + { + if( !StringAlgo::match( target.c_str(), targetPattern ) ) + { + continue; + } + if( values.find( key ) != values.end() ) + { + result.push_back( target ); + } + } + + return result; +} + void Metadata::registerValue( IECore::TypeId typeId, IECore::InternedString key, IECore::ConstDataPtr value ) { registerValue( typeId, key, [value]( const GraphComponent * ){ return value; } ); diff --git a/src/GafferModule/MetadataBinding.cpp b/src/GafferModule/MetadataBinding.cpp index 357bc23c5f..ca0916d466 100644 --- a/src/GafferModule/MetadataBinding.cpp +++ b/src/GafferModule/MetadataBinding.cpp @@ -323,6 +323,16 @@ list registeredGraphComponentValuesDeprecated( const GraphComponent *target, boo return keysToList( keys ); } +list targetsWithMetadataWrapper( const IECore::StringAlgo::MatchPattern &targetPattern, IECore::InternedString key ) +{ + std::vector targets; + { + IECorePython::ScopedGILRelease gilRelease; + targets = Metadata::targetsWithMetadata( targetPattern, key ); + } + return keysToList( targets ); +} + list plugsWithMetadata( GraphComponent *root, const std::string &key, bool instanceOnly ) { std::vector plugs = Metadata::plugsWithMetadata( root, key, instanceOnly ); @@ -452,6 +462,14 @@ void GafferModule::bindMetadata() .def( "plugValueChangedSignal", (Metadata::PlugValueChangedSignal &(*)( Gaffer::Node * ) )&Metadata::plugValueChangedSignal, return_value_policy() ) .staticmethod( "plugValueChangedSignal" ) + .def( "targetsWithMetadata", &targetsWithMetadataWrapper, + ( + boost::python::arg( "targetPattern" ), + boost::python::arg( "key" ) + ) + ) + .staticmethod( "targetsWithMetadata" ) + .def( "plugsWithMetadata", &plugsWithMetadata, ( boost::python::arg( "root" ), diff --git a/src/GafferScene/StandardAttributes.cpp b/src/GafferScene/StandardAttributes.cpp index 6940921c2c..19cf186783 100644 --- a/src/GafferScene/StandardAttributes.cpp +++ b/src/GafferScene/StandardAttributes.cpp @@ -65,6 +65,7 @@ StandardAttributes::StandardAttributes( const std::string &name ) // light linking + /// \todo The default value is wrong - it should be "defaultLights". attributes->addChild( new Gaffer::NameValuePlug( "linkedLights", new IECore::StringData( "" ), false, "linkedLights" ) ); // light filter linking diff --git a/startup/GafferScene/arnoldAttributes.py b/startup/GafferScene/arnoldAttributes.py index b4dc457a6a..9d32a282c3 100644 --- a/startup/GafferScene/arnoldAttributes.py +++ b/startup/GafferScene/arnoldAttributes.py @@ -73,6 +73,7 @@ contribute to illumination by default. """, ) +Gaffer.Metadata.registerValue( "attribute:ai:visibility:shadow_group", "ui:scene:acceptsSetExpression", True ) Gaffer.Metadata.registerValue( "attribute:ai:visibility:diffuse_reflect", "label", "Diffuse Reflection" ) Gaffer.Metadata.registerValue( "attribute:ai:visibility:diffuse_reflect", "defaultValue", IECore.BoolData( True ) ) diff --git a/startup/GafferScene/standardAttributes.py b/startup/GafferScene/standardAttributes.py index eddcfb9dbf..54da294374 100644 --- a/startup/GafferScene/standardAttributes.py +++ b/startup/GafferScene/standardAttributes.py @@ -142,7 +142,7 @@ ) Gaffer.Metadata.registerValue( "attribute:linkedLights", "label", "Linked Lights" ) -Gaffer.Metadata.registerValue( "attribute:linkedLights", "defaultValue", IECore.StringData( "" ) ) +Gaffer.Metadata.registerValue( "attribute:linkedLights", "defaultValue", "defaultLights" ) Gaffer.Metadata.registerValue( "attribute:linkedLights", "description", @@ -152,6 +152,7 @@ all lights that contribute to illumination by default. """ ) +Gaffer.Metadata.registerValue( "attribute:linkedLights", "ui:scene:acceptsSetExpression", True ) Gaffer.Metadata.registerValue( "attribute:filteredLights", "label", "Filtered Lights" ) Gaffer.Metadata.registerValue( "attribute:filteredLights", "defaultValue", IECore.StringData( "" ) ) @@ -165,6 +166,7 @@ contribute to illumination by default. """ ) +Gaffer.Metadata.registerValue( "attribute:filteredLights", "ui:scene:acceptsSetExpression", True ) Gaffer.Metadata.registerValue( "attribute:gaffer:automaticInstancing", "label", "Automatic Instancing" ) Gaffer.Metadata.registerValue( "attribute:gaffer:automaticInstancing", "defaultValue", IECore.BoolData( True ) ) diff --git a/startup/GafferSceneUI/lightFilter.py b/startup/GafferSceneUI/lightFilter.py deleted file mode 100644 index b83b32687b..0000000000 --- a/startup/GafferSceneUI/lightFilter.py +++ /dev/null @@ -1,40 +0,0 @@ -########################################################################## -# -# Copyright (c) 2024, Cinesite VFX Ltd. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above -# copyright notice, this list of conditions and the following -# disclaimer. -# -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided with -# the distribution. -# -# * Neither the name of John Haddon nor the names of -# any other contributors to this software may be used to endorse or -# promote products derived from this software without specific prior -# written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS -# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -########################################################################## - -import Gaffer -import GafferSceneUI - -Gaffer.Metadata.registerValue( "attribute:filteredLights", "ui:scene:acceptsSetExpression", True )