Skip to content

Commit

Permalink
Merge pull request #5703 from murraystevenson/renderPassEditorPathGro…
Browse files Browse the repository at this point in the history
…uping

RenderPassEditor : Grouped display of render passes
  • Loading branch information
murraystevenson authored Feb 29, 2024
2 parents a980bfe + 73ed0b0 commit 5ac0652
Show file tree
Hide file tree
Showing 7 changed files with 387 additions and 39 deletions.
11 changes: 11 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ Improvements
------------

- Viewer : Added <kbd>Ctrl</kbd>+<kbd>PgUp</kbd> shortcut for displaying the RGBA image layer (or the first available layer if RGBA doesn't exist).
- RenderPassEditor :
- Added the ability to display render passes grouped in a hierarchy generated from the render pass name. The default grouping uses the first token delimited by "_" from the render pass name, such that render passes named "char_gafferBot" and "char_cow" would be displayed under a "/char" group, while "prop_ball" and "prop_box" would be displayed under a "/prop" group.
- Render pass grouping can be configured in a startup file by using `GafferSceneUI.RenderPassEditor.registerPathGroupingFunction( f )`, where `f` is a function that receives a render pass name and returns the path that the render pass should be grouped under.
- Grouped display can be enabled by default in a startup file by using `Gaffer.Metadata.registerValue( GafferSceneUI.RenderPassEditor.Settings, "displayGrouped", "userDefault", IECore.BoolData( True ) )`.
- Dragging cells selected from the "Name" column now provides a list of the selected render pass names, rather than their paths.

API
---

- ScenePath : Added automatic conversion of a list of Python strings to a ScenePath.
- RenderPassEditor : Added `registerPathGroupingFunction()` and `pathGroupingFunction()` methods.

1.3.12.0 (relative to 1.3.11.0)
========
Expand Down
14 changes: 14 additions & 0 deletions python/GafferSceneTest/ScenePlugTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,20 @@ def testAccessorOverloads( self ) :

self.assertRaises( TypeError, p["out"].boundHash, 10 )

self.assertEqual( p["out"].attributes( [ "plane" ] ), p["out"].attributes( IECore.InternedStringVectorData( [ "plane" ] ) ) )
self.assertEqual( p["out"].transform( [ "plane" ] ), p["out"].transform( IECore.InternedStringVectorData( [ "plane" ] ) ) )
self.assertEqual( p["out"].object( [ "plane" ] ), p["out"].object( IECore.InternedStringVectorData( [ "plane" ] ) ) )
self.assertEqual( p["out"].bound( [ "plane" ] ), p["out"].bound( IECore.InternedStringVectorData( [ "plane" ] ) ) )
self.assertEqual( p["out"].childNames( [ "plane" ] ), p["out"].childNames( IECore.InternedStringVectorData( [ "plane" ] ) ) )

self.assertEqual( p["out"].attributesHash( [ "plane" ] ), p["out"].attributesHash( IECore.InternedStringVectorData( [ "plane" ] ) ) )
self.assertEqual( p["out"].transformHash( [ "plane" ] ), p["out"].transformHash( IECore.InternedStringVectorData( [ "plane" ] ) ) )
self.assertEqual( p["out"].objectHash( [ "plane" ] ), p["out"].objectHash( IECore.InternedStringVectorData( [ "plane" ] ) ) )
self.assertEqual( p["out"].boundHash( [ "plane" ] ), p["out"].boundHash( IECore.InternedStringVectorData( [ "plane" ] ) ) )
self.assertEqual( p["out"].childNamesHash( [ "plane" ] ), p["out"].childNamesHash( IECore.InternedStringVectorData( [ "plane" ] ) ) )

self.assertRaises( TypeError, p["out"].boundHash, [ 1 ] )

def testBoxPromotion( self ) :

b = Gaffer.Box()
Expand Down
137 changes: 119 additions & 18 deletions python/GafferSceneUI/RenderPassEditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def __init__( self ) :
self["tabGroup"] = Gaffer.StringPlug( defaultValue = "Cycles" )
self["section"] = Gaffer.StringPlug( defaultValue = "Main" )
self["editScope"] = Gaffer.Plug()
self["displayGrouped"] = Gaffer.BoolPlug()

IECore.registerRunTimeTyped( Settings, typeName = "GafferSceneUI::RenderPassEditor::Settings" )

Expand Down Expand Up @@ -107,6 +108,13 @@ def __init__( self, scriptNode, **kw ) :

with GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) :

GafferUI.PlugLayout(
self.__settingsNode,
orientation = GafferUI.ListContainer.Orientation.Horizontal,
rootSection = "Grouping"
)
GafferUI.Divider( orientation = GafferUI.Divider.Orientation.Vertical )

_SearchFilterWidget( searchFilter )
GafferUI.BasicPathFilterWidget( disabledRenderPassFilter )

Expand Down Expand Up @@ -145,6 +153,7 @@ def __init__( self, scriptNode, **kw ) :
self.__pathListing.keyPressSignal().connect( Gaffer.WeakMethod( self.__keyPress ), scoped = False )
self.__pathListing.buttonPressSignal().connectFront( Gaffer.WeakMethod( self.__buttonPress ), scoped = False )
self.__pathListing.selectionChangedSignal().connect( Gaffer.WeakMethod( self.__selectionChanged ), scoped = False )
self.__pathListing.dragBeginSignal().connectFront( Gaffer.WeakMethod( self.__dragBegin ), scoped = False )

self.__settingsNode.plugSetSignal().connect( Gaffer.WeakMethod( self.__settingsPlugSet ), scoped = False )
self.__settingsNode.plugInputChangedSignal().connect( Gaffer.WeakMethod( self.__settingsPlugInputChanged ), scoped = False )
Expand Down Expand Up @@ -190,6 +199,41 @@ def registerColumn( cls, groupKey, columnKey, inspectorFunction, section = "Main

section[columnKey] = inspectorFunction

__addRenderPassButtonMenuSignal = None
## This signal is emitted whenever the add render pass button is clicked.
# If the resulting menu definition has been populated with items,
# a popup menu will be presented from the button.
# If only a single item is present, its command will be called
# immediately instead of presenting a menu.
# If no items are present, then the default behaviour is to
# add a single new render pass with a user specified name.

@classmethod
def addRenderPassButtonMenuSignal( cls ) :

if cls.__addRenderPassButtonMenuSignal is None :
cls.__addRenderPassButtonMenuSignal = _AddButtonMenuSignal()

return cls.__addRenderPassButtonMenuSignal

## Registration of the function used to group render passes when
# `RenderPassEditor.Settings.displayGrouped` is enabled.
# 'f' should be a callable that takes a render pass name and returns
# a string or list of strings containing the path names that the
# render pass should be grouped under.
# For example: If "char_gafferBot_beauty" should be displayed grouped
# under `/char/gafferBot`, then `f( "char_gafferBot_beauty" )` should
# return `"/char/gafferBot" or `[ "char", "gafferBot" ]`.
@staticmethod
def registerPathGroupingFunction( f ) :

_GafferSceneUI._RenderPassEditor.RenderPassPath.registerPathGroupingFunction( f )

@staticmethod
def pathGroupingFunction() :

return _GafferSceneUI._RenderPassEditor.RenderPassPath.pathGroupingFunction()

def __repr__( self ) :

return "GafferSceneUI.RenderPassEditor( scriptNode )"
Expand Down Expand Up @@ -257,6 +301,8 @@ def __settingsPlugSet( self, plug ) :

if plug in ( self.__settingsNode["section"], self.__settingsNode["tabGroup"] ) :
self.__updateColumns()
elif plug == self.__settingsNode["displayGrouped"] :
self.__displayGroupedChanged()

def __settingsPlugInputChanged( self, plug ) :

Expand All @@ -280,10 +326,38 @@ def __setPathListingPath( self ) :
# control of updates ourselves in _updateFromContext(), using LazyMethod to defer the calls to this
# function until we are visible and playback has stopped.
contextCopy = Gaffer.Context( self.getContext() )
self.__pathListing.setPath( _GafferSceneUI._RenderPassEditor.RenderPassPath( self.__settingsNode["in"], contextCopy, "/", filter = self.__filter ) )
self.__pathListing.setPath( _GafferSceneUI._RenderPassEditor.RenderPassPath( self.__settingsNode["in"], contextCopy, "/", filter = self.__filter, grouped = self.__settingsNode["displayGrouped"].getValue() ) )
else :
self.__pathListing.setPath( Gaffer.DictPath( {}, "/" ) )

def __displayGroupedChanged( self ) :

selection = self.__pathListing.getSelection()
renderPassPath = self.__pathListing.getPath().copy()
grouped = self.__settingsNode["displayGrouped"].getValue()

# Remap selection so it is maintained when switching to/from grouped display
for i, pathMatcher in enumerate( selection ) :
remappedPaths = IECore.PathMatcher()
for path in pathMatcher.paths() :
renderPassPath.setFromString( path )
renderPassName = renderPassPath.property( "renderPassPath:name" )
if renderPassName is None :
continue

if grouped :
newPath = GafferScene.ScenePlug.stringToPath( self.pathGroupingFunction()( renderPassName ) )
newPath.append( renderPassName )
else :
newPath = renderPassName

remappedPaths.addPath( newPath )

selection[i] = remappedPaths

self.__setPathListingPath()
self.__pathListing.setSelection( selection )

def __buttonDoubleClick( self, pathListing, event ) :

# A small corner area below the vertical scroll bar may pass through
Expand Down Expand Up @@ -339,6 +413,13 @@ def __selectedRenderPasses( self, columns = [ 0 ] ) :

return list( result )

def __dragBegin( self, widget, event ) :

# Return render pass names rather than the path when dragging the Name column.
selection = self.__pathListing.getSelection()[0]
if not selection.isEmpty() :
return IECore.StringVectorData( self.__selectedRenderPasses() )

def __setActiveRenderPass( self, pathListing ) :

selectedPassNames = self.__selectedRenderPasses( columns = [ 1 ] )
Expand Down Expand Up @@ -825,23 +906,6 @@ def __updateButtonStatus( self, *unused ) :
self.__addButton.setEnabled( editable )
self.__addButton.setToolTip( "Click to add render pass." if editable else "To add a render pass, first choose an editable Edit Scope." )

__addRenderPassButtonMenuSignal = None
## This signal is emitted whenever the add render pass button is clicked.
# If the resulting menu definition has been populated with items,
# a popup menu will be presented from the button.
# If only a single item is present, its command will be called
# immediately instead of presenting a menu.
# If no items are present, then the default behaviour is to
# add a single new render pass with a user specified name.

@classmethod
def addRenderPassButtonMenuSignal( cls ) :

if cls.__addRenderPassButtonMenuSignal is None :
cls.__addRenderPassButtonMenuSignal = _AddButtonMenuSignal()

return cls.__addRenderPassButtonMenuSignal

GafferUI.Editor.registerType( "RenderPassEditor", RenderPassEditor )

##########################################################################
Expand Down Expand Up @@ -886,6 +950,18 @@ def addRenderPassButtonMenuSignal( cls ) :

],

"displayGrouped" : [

"description",
"""
Click to toggle between list and grouped display of render passes.
""",

"layout:section", "Grouping",
"plugValueWidget:type", "GafferSceneUI.RenderPassEditor._ToggleGroupingPlugValueWidget",

],

}

)
Expand Down Expand Up @@ -966,6 +1042,31 @@ def __init__( self, settingsNode, **kw ) :

RenderPassEditor._Spacer = _Spacer

## \todo Should this be a new displayMode of BoolPlugValueWidget?
class _ToggleGroupingPlugValueWidget( GafferUI.PlugValueWidget ) :

def __init__( self, plugs, **kw ) :

self.__row = GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 )

GafferUI.PlugValueWidget.__init__( self, self.__row, plugs )

self.__groupingModeButton = GafferUI.Button( image = "pathListingList.png", hasFrame=False )
self.__groupingModeButton.clickedSignal().connect( Gaffer.WeakMethod( self.__groupingModeButtonClicked ), scoped = False )
self.__row.append(
self.__groupingModeButton
)

def __groupingModeButtonClicked( self, button ) :

[ plug.setValue( not plug.getValue() ) for plug in self.getPlugs() ]

def _updateFromValues( self, values, exception ) :

self.__groupingModeButton.setImage( "pathListingTree.png" if all( values ) else "pathListingList.png" )

RenderPassEditor._ToggleGroupingPlugValueWidget = _ToggleGroupingPlugValueWidget

##########################################################################
# _SearchFilterWidget
##########################################################################
Expand Down
84 changes: 84 additions & 0 deletions python/GafferSceneUITest/RenderPassEditorTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

import Gaffer
import GafferScene
import GafferSceneUI
import GafferUITest

from GafferSceneUI import _GafferSceneUI
Expand Down Expand Up @@ -189,3 +190,86 @@ def testDisabledRenderPassFilter( self ) :
disabledRenderPassFilter.setEnabled( False )

self.assertEqual( [ str( c ) for c in path.children() ], [ "/A", "/B", "/C", "/D" ] )

def testPathGroupingFunction( self ) :

renderPasses = GafferScene.RenderPasses()
renderPasses["names"].setValue( IECore.StringVectorData( ["char_bot_beauty", "char_bot_shadow"] ) )

context = Gaffer.Context()
path = _GafferSceneUI._RenderPassEditor.RenderPassPath( renderPasses["out"], context, "/" )

self.assertEqual( [ str( c ) for c in path.children() ], [ "/char_bot_beauty", "/char_bot_shadow" ] )

def testFn( name ) :
return "/".join( name.split( "_" )[:-1] )

# Register our grouping function and test a grouped path
GafferSceneUI.RenderPassEditor.registerPathGroupingFunction( testFn )
self.assertEqual( testFn( "/char_bot_beauty" ), GafferSceneUI.RenderPassEditor.pathGroupingFunction()( "/char_bot_beauty" ) )

path = _GafferSceneUI._RenderPassEditor.RenderPassPath( renderPasses["out"], context, "/", grouped = True )

for parent, children in [
( "/", [ "/char" ] ),
( "/char", [ "/char/bot" ] ),
( "/char/bot", [ "/char/bot/char_bot_beauty", "/char/bot/char_bot_shadow" ] ),
( "/char/bot/char_bot_beauty", [] ),
( "/char/bot/char_bot_shadow", [] ),
] :

path.setFromString( parent )
self.assertTrue( path.isValid() )
self.assertEqual( path.isLeaf(), children == [] )
self.assertEqual( [ str( c ) for c in path.children() ], children )

# Ensure we can still get a flat output
path = _GafferSceneUI._RenderPassEditor.RenderPassPath( renderPasses["out"], context, "/", grouped = False )
self.assertEqual( [ str( c ) for c in path.children() ], [ "/char_bot_beauty", "/char_bot_shadow" ] )

def testDisabledRenderPassFilterWithPathGroupingFunction( self ) :

renderPasses = GafferScene.RenderPasses()
renderPasses["names"].setValue( IECore.StringVectorData( ["A_A", "A_B", "B_C", "B_D"] ) )

disablePass = GafferScene.CustomOptions( "disablePass" )
disablePass["in"].setInput( renderPasses["out"] )
disablePass["options"].addChild( Gaffer.NameValuePlug( "renderPass:enabled", Gaffer.BoolPlug( "value", defaultValue = False ), True, "member1" ) )

# disable A_B, B_C, B_D
switch = Gaffer.NameSwitch()
switch.setup( renderPasses["out"] )
switch["selector"].setValue( "${renderPass}" )
switch["in"]["in0"]["value"].setInput( renderPasses["out"] )
switch["in"]["in1"]["value"].setInput( disablePass["out"] )
switch["in"]["in1"]["name"].setValue( "A_B B_C B_D" )

def testFn( name ) :
return name.split( "_" )[:-1]

GafferSceneUI.RenderPassEditor.registerPathGroupingFunction( testFn )
context = Gaffer.Context()
path = _GafferSceneUI._RenderPassEditor.RenderPassPath( switch["out"]["value"], context, "/", grouped = True )

self.assertEqual( [ str( c ) for c in path.children() ], [ "/A", "/B" ] )

pathCopy = path.copy()
for p in [ "/A/A_A", "/A/A_B", "/B/B_C", "/B/B_D" ] :
pathCopy.setFromString( p )
self.assertEqual( pathCopy.property( "renderPassPath:enabled" ), p == "/A/A_A" )

disabledRenderPassFilter = _GafferSceneUI._RenderPassEditor.DisabledRenderPassFilter()
path.setFilter( disabledRenderPassFilter )
# We should only see /A, as both of /B's children are disabled
self.assertEqual( [ str( c ) for c in path.children() ], [ "/A" ] )
path.setFromString( "/A" )
self.assertEqual( [ str( c ) for c in path.children() ], [ "/A/A_A" ] )

# Disabling the filter should restore all paths
disabledRenderPassFilter.setEnabled( False )
path.setFromString( "/" )
self.assertEqual( [ str( c ) for c in path.children() ], [ "/A", "/B" ] )
path.setFromString( "/A" )
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" ] )
Loading

0 comments on commit 5ac0652

Please sign in to comment.