diff --git a/Changes.md b/Changes.md index bbd386b062d..131125755a3 100644 --- a/Changes.md +++ b/Changes.md @@ -11,6 +11,7 @@ Improvements - VisualiserTool : Changed `dataName` input widget for choosing the primitive variable to visualise to a list of available variable names for the current selection. - Tweaks nodes : Moved list of tweaks to a collapsible "Tweaks" section in the NodeEditor. +- GafferDispatchTest : Added GraphvizDispatcher for outputting batch graphs in Graphviz DOT format. It can be added to a script using the Python editor : `import GafferDispatchTest; root.addChild( GafferDispatchTest.GraphvizDispatcher())`. Fixes ----- diff --git a/python/GafferDispatchTest/GraphvizDispatcher.py b/python/GafferDispatchTest/GraphvizDispatcher.py new file mode 100644 index 00000000000..978df5b83ae --- /dev/null +++ b/python/GafferDispatchTest/GraphvizDispatcher.py @@ -0,0 +1,175 @@ +########################################################################## +# +# Copyright (c) 2025, 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 Image Engine Design Inc 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 IECore + +import Gaffer +import GafferDispatch + +class GraphvizDispatcher( GafferDispatch.Dispatcher ) : + + def __init__( self, name = "GraphvizDispatcher" ) : + + GafferDispatch.Dispatcher.__init__( self, name ) + + self["fileName"] = Gaffer.StringPlug() + self["includeFrames"] = Gaffer.BoolPlug( defaultValue = True ) + self["contextVariables"] = Gaffer.StringPlug() + + def __walkBatches( self, batch, outFile, scriptNode ) : + + if "id" in batch.blindData() : + return + + nodeName = batch.plug().node().relativeName( scriptNode ) if batch.plug() is not None else "root" + nodeType = batch.plug().node().typeName() if batch.plug() is not None else "None" + batch.blindData()["id"] = ( + nodeName + batch.context().hash().toString() + ) if batch.context() is not None else IECore.MurmurHash().toString() + + if nodeName == "root" : + outFile.write( "\t{} [shape=oval label=<{}>];\n".format( batch.blindData()["id"], nodeName ) ) + else : + + frameString = None + if self["includeFrames"].getValue() : + frameString = "-" + frameList = IECore.frameListFromList( [ int( i ) for i in batch.frames() ] ) + if len( frameList.asList() ) > 0 : + if frameList.start == frameList.end : + frameString = str( frameList.start ) + elif frameList.step != 1 : + frameString = "{} - {} x {}".format( frameList.start, frameList.end, frameList.step ) + else : + frameString = "{} - {}".format( frameList.start, frameList.end ) + + contextVariables = {} + + if batch.context() is not None : + scriptContext = scriptNode.context() + matchPattern = self["contextVariables"].getValue() + for entry in [ k for k in batch.context().keys() if k != "frame" and not k.startswith( "ui:" ) and not k.startswith( "dispatcher:" ) ] : + if IECore.StringAlgo.matchMultiple( entry, matchPattern ) and ( entry not in scriptContext.keys() or batch.context()[entry] != scriptContext[entry] ) : + contextVariables[entry] = batch.context().substitute( str( batch.context()[entry] ) ) + + batchString = '
| {} | |
| {} | |
| Frames: | {} |
| Context Variables | " + batchString += ""
+ batchString += "".join(
+ [ "{} = {} ".format( k, v ) for k, v in contextVariables.items() ] + ) + batchString += ' |