From a39cdf0a1b10b0731c527a319e7fdc15414c41fe Mon Sep 17 00:00:00 2001 From: John Haddon Date: Tue, 10 Dec 2024 15:43:15 +0000 Subject: [PATCH 1/3] ArrayPlug : Fix loading of dynamic ArrayPlugs saved in Gaffer 1.5+ We changed the name of the `element` argument, and need to account for that in a compatibility shim. A further complication is that in 1.5 the ArrayPlug doesn't care what direction the prototype has, because it is just used as a factory via `createCounterpart()` which re-specifies the direction. But in 1.4 it is used as the first element of the array as well, so we need to adjust direction if necessary. --- Changes.md | 1 + python/GafferSceneTest/ParentTest.py | 13 ++++ .../scripts/promotedArrayPlug-1.5.1.0.gfr | 76 +++++++++++++++++++ python/GafferTest/ArrayPlugTest.py | 14 +++- .../arrayPlugWithOutputPrototype-1.5.1.0.gfr | 31 ++++++++ startup/Gaffer/arrayPlugCompatibility.py | 59 ++++++++++++++ 6 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 python/GafferSceneTest/scripts/promotedArrayPlug-1.5.1.0.gfr create mode 100644 python/GafferTest/scripts/arrayPlugWithOutputPrototype-1.5.1.0.gfr create mode 100644 startup/Gaffer/arrayPlugCompatibility.py diff --git a/Changes.md b/Changes.md index 005da9cfb71..af56a72d95b 100644 --- a/Changes.md +++ b/Changes.md @@ -4,6 +4,7 @@ Fixes ----- +- ArrayPlug : Fixed loading of promoted plugs saved from Gaffer 1.5+. - GraphEditor : Fixed errors when dragging an unknown file type into the GraphEditor. - Widget : Fixed `event.sourceWidget` for DragDropEvents generated from a Qt native drag within the same Gaffer process. This will now reference the `GafferUI.Widget` that the Qt source widget belongs to, if any. - Catalogue : Fixed bug which "stole" drags that crossed the image listing but which were destined elsewhere, for instance a drag from the HierarchyView to a PathFilter in the GraphEditor. diff --git a/python/GafferSceneTest/ParentTest.py b/python/GafferSceneTest/ParentTest.py index eb46adb1b64..bb5fedb54c0 100644 --- a/python/GafferSceneTest/ParentTest.py +++ b/python/GafferSceneTest/ParentTest.py @@ -490,6 +490,19 @@ def testLoadPromotedChildrenPlug( self ) : s2["b2"]["p"]["children"].getInput().fullName() ) + def testLoadPromotedChildrenPlugFrom1_5( self ) : + + script = Gaffer.ScriptNode() + script["fileName"].setValue( pathlib.Path( __file__ ).parent / "scripts" / "promotedArrayPlug-1.5.1.0.gfr" ) + script.load() + + self.assertEqual( script["Box"]["children"][0].getInput(), script["Cube"]["out"] ) + self.assertEqual( script["Box"]["Parent"]["children"].source(), script["Box"]["children"] ) + self.assertEqual( script["Box"]["Parent"]["children"][0].source(), script["Cube"]["out"] ) + + self.assertSceneValid( script["Box"]["out"] ) + self.assertEqual( script["Box"]["out"].childNames( "/" ), IECore.InternedStringVectorData( [ "sphere", "cube" ] ) ) + def testSetPassThroughWhenNoParent( self ) : sphere = GafferScene.Sphere() diff --git a/python/GafferSceneTest/scripts/promotedArrayPlug-1.5.1.0.gfr b/python/GafferSceneTest/scripts/promotedArrayPlug-1.5.1.0.gfr new file mode 100644 index 00000000000..e0845ab730e --- /dev/null +++ b/python/GafferSceneTest/scripts/promotedArrayPlug-1.5.1.0.gfr @@ -0,0 +1,76 @@ +import Gaffer +import GafferImage +import GafferScene +import imath + +Gaffer.Metadata.registerValue( parent, "serialiser:milestoneVersion", 1, persistent=False ) +Gaffer.Metadata.registerValue( parent, "serialiser:majorVersion", 5, persistent=False ) +Gaffer.Metadata.registerValue( parent, "serialiser:minorVersion", 1, persistent=False ) +Gaffer.Metadata.registerValue( parent, "serialiser:patchVersion", 0, persistent=False ) + +__children = {} + +parent["variables"].addChild( Gaffer.NameValuePlug( "image:catalogue:port", Gaffer.IntPlug( "value", defaultValue = 0, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ), "imageCataloguePort", Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) ) +parent["variables"].addChild( Gaffer.NameValuePlug( "project:name", Gaffer.StringPlug( "value", defaultValue = 'default', flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ), "projectName", Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) ) +parent["variables"].addChild( Gaffer.NameValuePlug( "project:rootDirectory", Gaffer.StringPlug( "value", defaultValue = '$HOME/gaffer/projects/${project:name}', flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ), "projectRootDirectory", Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) ) +__children["openColorIO"] = GafferImage.OpenColorIOConfigPlug( "openColorIO", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) +parent.addChild( __children["openColorIO"] ) +__children["defaultFormat"] = GafferImage.FormatPlug( "defaultFormat", defaultValue = GafferImage.Format( 1920, 1080, 1.000 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) +parent.addChild( __children["defaultFormat"] ) +__children["Box"] = Gaffer.Box( "Box" ) +parent.addChild( __children["Box"] ) +__children["Box"].addChild( GafferScene.Parent( "Parent" ) ) +__children["Box"]["Parent"]["children"].resize( 2 ) +__children["Box"]["Parent"].addChild( Gaffer.V2fPlug( "__uiPosition", defaultValue = imath.V2f( 0, 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["Box"].addChild( Gaffer.V2fPlug( "__uiPosition", defaultValue = imath.V2f( 0, 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["Box"].addChild( Gaffer.BoxIn( "BoxIn" ) ) +__children["Box"]["BoxIn"].setup( Gaffer.ArrayPlug( "out", elementPrototype = GafferScene.ScenePlug( "child0", ), ) ) +__children["Box"]["BoxIn"]["__in"].resize( 2 ) +__children["Box"]["BoxIn"]["out"].resize( 2 ) +__children["Box"]["BoxIn"].addChild( Gaffer.V2fPlug( "__uiPosition", defaultValue = imath.V2f( 0, 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["Box"].addChild( Gaffer.ArrayPlug( "children", elementPrototype = GafferScene.ScenePlug( "child0", ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["Box"]["children"].resize( 2 ) +__children["Box"].addChild( GafferScene.Sphere( "Sphere" ) ) +__children["Box"]["Sphere"].addChild( Gaffer.V2fPlug( "__uiPosition", defaultValue = imath.V2f( 0, 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["Box"].addChild( Gaffer.BoxOut( "BoxOut" ) ) +__children["Box"]["BoxOut"].setup( GafferScene.ScenePlug( "in", ) ) +__children["Box"]["BoxOut"].addChild( Gaffer.V2fPlug( "__uiPosition", defaultValue = imath.V2f( 0, 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["Box"].addChild( GafferScene.ScenePlug( "out", direction = Gaffer.Plug.Direction.Out, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["Cube"] = GafferScene.Cube( "Cube" ) +parent.addChild( __children["Cube"] ) +__children["Cube"].addChild( Gaffer.V2fPlug( "__uiPosition", defaultValue = imath.V2f( 0, 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +parent["variables"]["imageCataloguePort"]["value"].setValue( 39173 ) +Gaffer.Metadata.registerValue( parent["variables"]["imageCataloguePort"], 'readOnly', True ) +Gaffer.Metadata.registerValue( parent["variables"]["projectName"]["name"], 'readOnly', True ) +Gaffer.Metadata.registerValue( parent["variables"]["projectRootDirectory"]["name"], 'readOnly', True ) +__children["Box"]["Parent"]["in"].setInput( __children["Box"]["Sphere"]["out"] ) +__children["Box"]["Parent"]["parent"].setValue( '/' ) +__children["Box"]["Parent"]["children"].setInput( __children["Box"]["BoxIn"]["out"] ) +Gaffer.Metadata.registerValue( __children["Box"]["Parent"]["children"], 'nodule:type', 'GafferUI::StandardNodule' ) +__children["Box"]["Parent"]["__uiPosition"].setValue( imath.V2f( -9.90000057, 4.5 ) ) +__children["Box"]["__uiPosition"].setValue( imath.V2f( -16.6000023, 1.09999955 ) ) +__children["Box"]["BoxIn"]["name"].setValue( 'children' ) +__children["Box"]["BoxIn"]["__in"].setInput( __children["Box"]["children"] ) +Gaffer.Metadata.registerValue( __children["Box"]["BoxIn"]["__in"], 'nodule:type', 'GafferUI::CompoundNodule' ) +Gaffer.Metadata.registerValue( __children["Box"]["BoxIn"]["__in"], 'description', 'The child hierarchies to be parented.' ) +Gaffer.Metadata.registerValue( __children["Box"]["BoxIn"]["__in"], 'plugValueWidget:type', '' ) +Gaffer.Metadata.registerValue( __children["Box"]["BoxIn"]["__in"], 'noduleLayout:spacing', 0.5 ) +Gaffer.Metadata.registerValue( __children["Box"]["BoxIn"]["out"], 'nodule:type', 'GafferUI::StandardNodule' ) +__children["Box"]["BoxIn"]["__uiPosition"].setValue( imath.V2f( -8.40068531, 12.8320312 ) ) +Gaffer.Metadata.registerValue( __children["Box"]["children"], 'nodule:type', 'GafferUI::CompoundNodule' ) +Gaffer.Metadata.registerValue( __children["Box"]["children"], 'description', 'The child hierarchies to be parented.' ) +Gaffer.Metadata.registerValue( __children["Box"]["children"], 'plugValueWidget:type', '' ) +Gaffer.Metadata.registerValue( __children["Box"]["children"], 'noduleLayout:spacing', 0.5 ) +__children["Box"]["children"][0].setInput( __children["Cube"]["out"] ) +__children["Box"]["Sphere"]["__uiPosition"].setValue( imath.V2f( -24.5000019, 11.3000002 ) ) +__children["Box"]["BoxOut"]["in"].setInput( __children["Box"]["Parent"]["out"] ) +Gaffer.Metadata.registerValue( __children["Box"]["BoxOut"]["__out"], 'description', 'The processed output scene.' ) +Gaffer.Metadata.registerValue( __children["Box"]["BoxOut"]["__out"], 'nodule:type', 'GafferUI::StandardNodule' ) +__children["Box"]["BoxOut"]["__uiPosition"].setValue( imath.V2f( -8.40068531, -3.83203101 ) ) +__children["Box"]["out"].setInput( __children["Box"]["BoxOut"]["__out"] ) +Gaffer.Metadata.registerValue( __children["Box"]["out"], 'description', 'The processed output scene.' ) +Gaffer.Metadata.registerValue( __children["Box"]["out"], 'nodule:type', 'GafferUI::StandardNodule' ) +__children["Cube"]["__uiPosition"].setValue( imath.V2f( -18.8500023, 9.43203068 ) ) + + +del __children diff --git a/python/GafferTest/ArrayPlugTest.py b/python/GafferTest/ArrayPlugTest.py index a0d5d80b726..c0ad7307588 100644 --- a/python/GafferTest/ArrayPlugTest.py +++ b/python/GafferTest/ArrayPlugTest.py @@ -34,8 +34,9 @@ # ########################################################################## +import pathlib import unittest -import gc + import imath import IECore @@ -537,5 +538,16 @@ def testResizeWithoutExistingChildren( self ) : with self.assertRaisesRegex( RuntimeError, "Can't resize ArrayPlug `p` as it has no children" ) : p.resize( 1 ) + def testLoadOutputPrototypeFrom1_5( self ) : + + script = Gaffer.ScriptNode() + script["fileName"].setValue( pathlib.Path( __file__ ).parent / "scripts" / "arrayPlugWithOutputPrototype-1.5.1.0.gfr" ) + script.load() + + self.assertIsInstance( script["Node"]["user"]["array"], Gaffer.ArrayPlug ) + self.assertEqual( script["Node"]["user"]["array"].direction(), Gaffer.Plug.Direction.In ) + self.assertEqual( len( script["Node"]["user"]["array"] ), 1 ) + self.assertIsInstance( script["Node"]["user"]["array"][0], Gaffer.IntPlug ) + if __name__ == "__main__": unittest.main() diff --git a/python/GafferTest/scripts/arrayPlugWithOutputPrototype-1.5.1.0.gfr b/python/GafferTest/scripts/arrayPlugWithOutputPrototype-1.5.1.0.gfr new file mode 100644 index 00000000000..23ebf99f5cd --- /dev/null +++ b/python/GafferTest/scripts/arrayPlugWithOutputPrototype-1.5.1.0.gfr @@ -0,0 +1,31 @@ +import Gaffer +import GafferImage +import imath + +Gaffer.Metadata.registerValue( parent, "serialiser:milestoneVersion", 1, persistent=False ) +Gaffer.Metadata.registerValue( parent, "serialiser:majorVersion", 5, persistent=False ) +Gaffer.Metadata.registerValue( parent, "serialiser:minorVersion", 1, persistent=False ) +Gaffer.Metadata.registerValue( parent, "serialiser:patchVersion", 0, persistent=False ) + +__children = {} + +parent["variables"].addChild( Gaffer.NameValuePlug( "image:catalogue:port", Gaffer.IntPlug( "value", defaultValue = 0, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ), "imageCataloguePort", Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) ) +parent["variables"].addChild( Gaffer.NameValuePlug( "project:name", Gaffer.StringPlug( "value", defaultValue = 'default', flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ), "projectName", Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) ) +parent["variables"].addChild( Gaffer.NameValuePlug( "project:rootDirectory", Gaffer.StringPlug( "value", defaultValue = '$HOME/gaffer/projects/${project:name}', flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ), "projectRootDirectory", Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) ) +__children["openColorIO"] = GafferImage.OpenColorIOConfigPlug( "openColorIO", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) +parent.addChild( __children["openColorIO"] ) +__children["defaultFormat"] = GafferImage.FormatPlug( "defaultFormat", defaultValue = GafferImage.Format( 1920, 1080, 1.000 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) +parent.addChild( __children["defaultFormat"] ) +__children["Node"] = Gaffer.Node( "Node" ) +parent.addChild( __children["Node"] ) +__children["Node"]["user"].addChild( Gaffer.ArrayPlug( "array", elementPrototype = Gaffer.IntPlug( "IntPlug", direction = Gaffer.Plug.Direction.Out, defaultValue = 0, ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["Node"].addChild( Gaffer.V2fPlug( "__uiPosition", defaultValue = imath.V2f( 0, 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +parent["frame"].setValue( 100.0 ) +parent["variables"]["imageCataloguePort"]["value"].setValue( 41733 ) +Gaffer.Metadata.registerValue( parent["variables"]["imageCataloguePort"], 'readOnly', True ) +Gaffer.Metadata.registerValue( parent["variables"]["projectName"]["name"], 'readOnly', True ) +Gaffer.Metadata.registerValue( parent["variables"]["projectRootDirectory"]["name"], 'readOnly', True ) +__children["Node"]["__uiPosition"].setValue( imath.V2f( -9.40000057, 0.100000001 ) ) + + +del __children diff --git a/startup/Gaffer/arrayPlugCompatibility.py b/startup/Gaffer/arrayPlugCompatibility.py new file mode 100644 index 00000000000..3019f6954dc --- /dev/null +++ b/startup/Gaffer/arrayPlugCompatibility.py @@ -0,0 +1,59 @@ +########################################################################## +# +# 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 + +def __initWrapper( originalInit ) : + + def init( self, *args, **kw ) : + + if "elementPrototype" in kw : + + # We renamed `element` to `elementPrototype` in Gaffer 1.5. + kw["element"] = kw["elementPrototype"] + del kw["elementPrototype"] + # And because it's just a prototype in 1.5, its direction may not + # match what we need (because we're actually going to use it as a + # child). + direction = kw.get( "direction", Gaffer.Plug.Direction.In ) + if kw["element"].direction() != direction : + kw["element"] = kw["element"].createCounterpart( kw["element"].getName(), direction ) + + originalInit( self, *args, **kw ) + + return init + +Gaffer.ArrayPlug.__init__ = __initWrapper( Gaffer.ArrayPlug.__init__ ) From 1d8403c912e060f0a31a94ee676ed2e9335ea9e2 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Tue, 10 Dec 2024 16:44:25 +0000 Subject: [PATCH 2/3] RandomChoiceUI : Fix handling of non-ValuePlugs Right-clicking on (for example) a `ShaderAssignment.shader` plug in the NodeEditor was leading to the following warning : ``` ERROR : Plug menu : Traceback (most recent call last): ERROR : File "/home/john/dev/build/gaffer-1.4/python/GafferUI/PlugValueWidget.py", line 994, in __combiner ERROR : next( results ) ERROR : IECore.Exception: Traceback (most recent call last): ERROR : File "/home/john/dev/build/gaffer-1.4/python/GafferUI/RandomChoiceUI.py", line 245, in __popupMenu ERROR : if not Gaffer.RandomChoice.canSetup( plug ) : ERROR : Boost.Python.ArgumentError: Python argument types in ERROR : RandomChoice.canSetup(ShaderPlug) ERROR : did not match C++ signature: ERROR : canSetup(Gaffer::ValuePlug const*) ``` --- Changes.md | 1 + python/GafferUI/RandomChoiceUI.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Changes.md b/Changes.md index 005da9cfb71..acab6e1dce1 100644 --- a/Changes.md +++ b/Changes.md @@ -8,6 +8,7 @@ Fixes - Widget : Fixed `event.sourceWidget` for DragDropEvents generated from a Qt native drag within the same Gaffer process. This will now reference the `GafferUI.Widget` that the Qt source widget belongs to, if any. - Catalogue : Fixed bug which "stole" drags that crossed the image listing but which were destined elsewhere, for instance a drag from the HierarchyView to a PathFilter in the GraphEditor. - GadgetWidget : Fixed signal handling bug in `setViewportGadget()`. This could cause the widget to attempt to redraw unnecessarily when the _old_ viewport requested a redraw. +- RandomChoice : Fixed errors right-clicking on non-value plugs in the NodeEditor. 1.4.15.2 (relative to 1.4.15.1) ======== diff --git a/python/GafferUI/RandomChoiceUI.py b/python/GafferUI/RandomChoiceUI.py index 0a4704cbac7..3eb4f0bd4ae 100644 --- a/python/GafferUI/RandomChoiceUI.py +++ b/python/GafferUI/RandomChoiceUI.py @@ -240,6 +240,8 @@ def __popupMenu( menuDefinition, plugValueWidget ) : return for plug in plugValueWidget.getPlugs() : + if not isinstance( plug, Gaffer.ValuePlug ) : + return if plug.getInput() is not None or Gaffer.MetadataAlgo.readOnly( plug ) : return if not Gaffer.RandomChoice.canSetup( plug ) : From 97c574bc3ea746c951481ef72233fda9db4b99a2 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Fri, 13 Dec 2024 09:11:08 +0000 Subject: [PATCH 3/3] Bump version to 1.4.15.3 --- Changes.md | 7 ++++++- SConstruct | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Changes.md b/Changes.md index ef0dda8ab06..a27b39edff6 100644 --- a/Changes.md +++ b/Changes.md @@ -1,4 +1,9 @@ -1.4.15.x (relative to 1.4.15.2) +1.4.15.x (relative to 1.4.15.3) +======== + + + +1.4.15.3 (relative to 1.4.15.2) ======== Fixes diff --git a/SConstruct b/SConstruct index 7b6ec201560..7fc71aab863 100644 --- a/SConstruct +++ b/SConstruct @@ -64,7 +64,7 @@ if codecs.lookup( locale.getpreferredencoding() ).name != "utf-8" : gafferMilestoneVersion = 1 # for announcing major milestones - may contain all of the below gafferMajorVersion = 4 # backwards-incompatible changes gafferMinorVersion = 15 # new backwards-compatible features -gafferPatchVersion = 2 # bug fixes +gafferPatchVersion = 3 # bug fixes gafferVersionSuffix = "" # used for alpha/beta releases : "a1", "b2", etc. # All of the following must be considered when determining