diff --git a/Changes.md b/Changes.md index 3896ec7e576..d39c768429a 100644 --- a/Changes.md +++ b/Changes.md @@ -13,6 +13,10 @@ Fixes - Fixed partial image updates when an unrelated InteractiveRender was running (#6043). - Fixed "colour tearing", where updates to some image channels became visible before updates to others. - Fixed unnecessary texture updates when specific image tiles don't change. +- ArrayPlug : + - Fixed error when `resize()` removed plugs with input connections. + - Fixed error when `resize()` was used on an output plug. +- CreateViews : Fixed loading of files saved from Gaffer 1.5+. 1.4.13.0 (relative to 1.4.12.0) ======== @@ -818,6 +822,10 @@ Fixes - Fixed partial image updates when an unrelated InteractiveRender was running (#6043). - Fixed "colour tearing", where updates to some image channels became visible before updates to others. - Fixed unnecessary texture updates when specific image tiles don't change. +- ArrayPlug : + - Fixed error when `resize()` removed plugs with input connections. + - Fixed error when `resize()` was used on an output plug. +- CreateViews : Fixed loading of files saved from Gaffer 1.5+. 1.3.16.8 (relative to 1.3.16.7) ======== diff --git a/python/GafferImageTest/CreateViewsTest.py b/python/GafferImageTest/CreateViewsTest.py index 3db8b0252c5..cc6800d886f 100644 --- a/python/GafferImageTest/CreateViewsTest.py +++ b/python/GafferImageTest/CreateViewsTest.py @@ -38,6 +38,7 @@ import imath import inspect import os +import pathlib import IECore @@ -224,5 +225,18 @@ def testInputToExpressionDrivingEnabledPlug( self ) : # for this particular view. self.assertFalse( script["constant"]["enabled"].getValue() ) + def testLoadFromVersion1_5( self ) : + + script = Gaffer.ScriptNode() + script["fileName"].setValue( pathlib.Path( __file__ ).parent / "scripts" / "createViews-1.5.0.0.gfr" ) + script.load() + + self.assertEqual( len( script["CreateViews"]["views"] ), 2 ) + self.assertEqual( script["CreateViews"]["views"][0]["name"].getValue(), "left" ) + self.assertEqual( script["CreateViews"]["views"][1]["name"].getValue(), "right" ) + self.assertTrue( script["CreateViews"]["views"][0]["value"].getInput().isSame( script["Checkerboard"]["out"] ) ) + self.assertTrue( script["CreateViews"]["views"][1]["value"].getInput().isSame( script["Checkerboard1"]["out"] ) ) + self.assertEqual( script["CreateViews"]["out"].viewNames(), IECore.StringVectorData( [ "left", "right" ] ) ) + if __name__ == "__main__": unittest.main() diff --git a/python/GafferImageTest/scripts/createViews-1.5.0.0.gfr b/python/GafferImageTest/scripts/createViews-1.5.0.0.gfr new file mode 100644 index 00000000000..56b4bd1454a --- /dev/null +++ b/python/GafferImageTest/scripts/createViews-1.5.0.0.gfr @@ -0,0 +1,45 @@ +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", 0, 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["Checkerboard"] = GafferImage.Checkerboard( "Checkerboard" ) +parent.addChild( __children["Checkerboard"] ) +__children["Checkerboard"].addChild( Gaffer.V2fPlug( "__uiPosition", defaultValue = imath.V2f( 0, 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["Checkerboard1"] = GafferImage.Checkerboard( "Checkerboard1" ) +parent.addChild( __children["Checkerboard1"] ) +__children["Checkerboard1"].addChild( Gaffer.V2fPlug( "__uiPosition", defaultValue = imath.V2f( 0, 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["CreateViews"] = GafferImage.CreateViews( "CreateViews" ) +parent.addChild( __children["CreateViews"] ) +__children["CreateViews"]["views"].resize( 2 ) +__children["CreateViews"].addChild( Gaffer.V2fPlug( "__uiPosition", defaultValue = imath.V2f( 0, 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +parent["variables"]["imageCataloguePort"]["value"].setValue( 34907 ) +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["Checkerboard"]["size"]["y"].setInput( __children["Checkerboard"]["size"]["x"] ) +__children["Checkerboard"]["__uiPosition"].setValue( imath.V2f( -30.6000004, 10.4000006 ) ) +__children["Checkerboard1"]["size"]["y"].setInput( __children["Checkerboard1"]["size"]["x"] ) +__children["Checkerboard1"]["__uiPosition"].setValue( imath.V2f( -16.8000031, 10.5 ) ) +__children["CreateViews"]["views"][0]["name"].setValue( 'left' ) +__children["CreateViews"]["views"][0]["value"].setInput( __children["Checkerboard"]["out"] ) +__children["CreateViews"]["views"][1]["name"].setValue( 'right' ) +__children["CreateViews"]["views"][1]["value"].setInput( __children["Checkerboard1"]["out"] ) +__children["CreateViews"]["__uiPosition"].setValue( imath.V2f( -23.8000011, 2.23593783 ) ) + + +del __children + diff --git a/python/GafferTest/ArrayPlugTest.py b/python/GafferTest/ArrayPlugTest.py index 4c75d9881e6..a0d5d80b726 100644 --- a/python/GafferTest/ArrayPlugTest.py +++ b/python/GafferTest/ArrayPlugTest.py @@ -494,6 +494,22 @@ def testResize( self ) : with self.assertRaises( RuntimeError ) : p.resize( p.maxSize() + 1 ) + def testRemoveInputDuringResize( self ) : + + node = Gaffer.Node() + node["user"]["p"] = Gaffer.IntPlug() + node["user"]["array"] = Gaffer.ArrayPlug( element = Gaffer.IntPlug(), resizeWhenInputsChange = True ) + node["user"]["array"].resize( 4 ) + node["user"]["array"][2].setInput( node["user"]["p"] ) + + node["user"]["array"].resize( 1 ) + self.assertEqual( len( node["user"]["array"] ), 1 ) + + def testResizeOutputPlug( self ) : + + array = Gaffer.ArrayPlug( element = Gaffer.IntPlug( direction = Gaffer.Plug.Direction.Out ), direction = Gaffer.Plug.Direction.Out ) + array.resize( 2 ) + def testSerialisationUsesIndices( self ) : s = Gaffer.ScriptNode() @@ -515,5 +531,11 @@ def testSerialisationUsesIndices( self ) : self.assertEqual( s2["n"]["in"][0].getInput(), s2["a"]["sum"] ) self.assertEqual( s2["n"]["in"][1].getInput(), s2["a"]["sum"] ) + def testResizeWithoutExistingChildren( self ) : + + p = Gaffer.ArrayPlug( name = "p" ) + with self.assertRaisesRegex( RuntimeError, "Can't resize ArrayPlug `p` as it has no children" ) : + p.resize( 1 ) + if __name__ == "__main__": unittest.main() diff --git a/src/Gaffer/ArrayPlug.cpp b/src/Gaffer/ArrayPlug.cpp index dd5bced0c3a..78f6000a98f 100644 --- a/src/Gaffer/ArrayPlug.cpp +++ b/src/Gaffer/ArrayPlug.cpp @@ -151,14 +151,39 @@ void ArrayPlug::resize( size_t size ) throw IECore::Exception( "Invalid size" ); } + if( size && !children().size() ) + { + if( auto scriptNode = ancestor() ) + { + if( scriptNode->isExecuting() ) + { + // Needed to allow CreateViews nodes serialised from Gaffer 1.5 + // to be loaded. In 1.5, CreateViews creates an ArrayPlug with + // an element prototype, and uses `resize()` in the + // serialisation. We can't resize here because we don't have a + // prototype, but as long as we don't error, + // `startup/GafferImage/createViewsCompatibility.py` will deal + // with the rest for us. + return; + } + } + throw IECore::Exception( + fmt::format( + "Can't resize ArrayPlug `{}` as it has no children", + fullName() + ) + ); + } + while( size > children().size() ) { - PlugPtr p = getChild( 0 )->createCounterpart( getChild( 0 )->getName(), Plug::In ); + PlugPtr p = getChild( 0 )->createCounterpart( getChild( 0 )->getName(), direction() ); p->setFlags( Gaffer::Plug::Dynamic, true ); addChild( p ); MetadataAlgo::copyColors( getChild( 0 ) , p.get() , /* overwrite = */ false ); } + Gaffer::Signals::BlockedConnection blockedInputChange( m_inputChangedConnection ); while( children().size() > size ) { removeChild( children().back() ); diff --git a/startup/GafferImage/createViewsCompatibility.py b/startup/GafferImage/createViewsCompatibility.py new file mode 100644 index 00000000000..fa199aa3cfe --- /dev/null +++ b/startup/GafferImage/createViewsCompatibility.py @@ -0,0 +1,56 @@ +########################################################################## +# +# 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 GafferImage + +def __arrayPlugGetItem( originalGetItem ) : + + def getItem( self, key ) : + + if isinstance( key, int ) and key >= len( self ) : + if isinstance( self.parent(), GafferImage.CreateViews ) and self.getName() == "views" : + # Probably loading a serialisation from Gaffer 1.5+, where `resize()` would + # have added the array elements that we are missing here. Just add them manually. + if not len( self ) : + self.addChild( Gaffer.NameValuePlug( "", GafferImage.ImagePlug(), True, "view0", Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) ) + self.resize( key + 1 ) + + return originalGetItem( self, key ) + + return getItem + +Gaffer.ArrayPlug.__getitem__ = __arrayPlugGetItem( Gaffer.ArrayPlug.__getitem__ )