From 318ae7ff42bf3846d44f7e5f35a8feca3fd9a1cd Mon Sep 17 00:00:00 2001 From: John Haddon Date: Mon, 11 Dec 2023 11:12:18 +0000 Subject: [PATCH 1/7] ShufflePlugValueWidget : Use widget metadata for `source` and `destination` And align all widgets to the top, so that everything still lines up if the custom widgets use additional vertical space. --- Changes.md | 1 + python/GafferUI/ShufflePlugValueWidget.py | 31 ++++++++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/Changes.md b/Changes.md index 4d6d01f14fe..8e9b6e2a7d4 100644 --- a/Changes.md +++ b/Changes.md @@ -49,6 +49,7 @@ API - GafferTractorTest : Added `tractorAPI()` method which returns a mock API if Tractor is not available. This allows the GafferTractor module to be tested without Tractor being installed. - ParallelAlgo : Added `canCallOnUIThread()` function. - Label : Added `textSelectable` constructor argument. +- ShufflePlugValueWidget : Widgets for the `source` and `destination` plugs can now be customised using standard `plugValueWidget:type` metadata. Breaking Changes ---------------- diff --git a/python/GafferUI/ShufflePlugValueWidget.py b/python/GafferUI/ShufflePlugValueWidget.py index bd5f2390d49..070b8c5250c 100644 --- a/python/GafferUI/ShufflePlugValueWidget.py +++ b/python/GafferUI/ShufflePlugValueWidget.py @@ -43,6 +43,8 @@ import Gaffer import GafferUI +from Qt import QtWidgets + ########################################################################## # ShufflePlug Widget ########################################################################## @@ -57,8 +59,11 @@ def __init__( self, plugs ) : GafferUI.PlugValueWidget.__init__( self, self.__row, plugs ) - sourceWidget = GafferUI.StringPlugValueWidget( { p["source"] for p in self.getPlugs() } ) - sourceWidget.textWidget()._qtWidget().setFixedWidth( GafferUI.PlugWidget.labelWidth() ) + sourceWidget = GafferUI.PlugValueWidget.create( { p["source"] for p in self.getPlugs() } ) + sourceWidget._qtWidget().setFixedWidth( GafferUI.PlugWidget.labelWidth() ) + if sourceWidget._qtWidget().layout() is not None : + sourceWidget._qtWidget().layout().setSizeConstraint( QtWidgets.QLayout.SetDefaultConstraint ) + self.__row.append( sourceWidget, verticalAlignment = GafferUI.Label.VerticalAlignment.Top ) self.__row.append( @@ -69,14 +74,28 @@ def __init__( self, plugs ) : verticalAlignment = GafferUI.Label.VerticalAlignment.Top, ) - destinationWidget = GafferUI.StringPlugValueWidget( { p["destination"] for p in self.getPlugs() } ) - destinationWidget.textWidget()._qtWidget().setFixedWidth( GafferUI.PlugWidget.labelWidth() ) + destinationWidget = GafferUI.PlugValueWidget.create( { p["destination"] for p in self.getPlugs() } ) + destinationWidget._qtWidget().setFixedWidth( GafferUI.PlugWidget.labelWidth() ) + if destinationWidget._qtWidget().layout() is not None : + destinationWidget._qtWidget().layout().setSizeConstraint( QtWidgets.QLayout.SetDefaultConstraint ) self.__row.append( destinationWidget, verticalAlignment = GafferUI.Label.VerticalAlignment.Top ) deleteSourceWidget = GafferUI.BoolPlugValueWidget( { p["deleteSource"] for p in self.getPlugs() } ) deleteSourceWidget.boolWidget()._qtWidget().setFixedWidth( GafferUI.PlugWidget.labelWidth() - 40 ) - self.__row.append( deleteSourceWidget ) - self.__row.append( GafferUI.BoolPlugValueWidget( { p["replaceDestination"] for p in self.getPlugs() } ) ) + self.__row.append( deleteSourceWidget, verticalAlignment = GafferUI.Label.VerticalAlignment.Top ) + self.__row.append( + GafferUI.BoolPlugValueWidget( { p["replaceDestination"] for p in self.getPlugs() } ), + verticalAlignment = GafferUI.Label.VerticalAlignment.Top, + ) + + # Work around annoying Qt behaviour whereby QHBoxLayout expands vertically if all children + # have a vertical alignment. This appears to ultimately be the fault of `qSmartMaxSize` in + # `qlayoutengine.cpp`, which sets the maximum height to `QLAYOUTSIZE_MAX` when there is _any_ + # vertical alignment flag set. + self.__row.append( + GafferUI.Spacer( imath.V2i( 0 ), maximumSize = imath.V2i( 0 ) ), + # Note : no `verticalAlignment` argument + ) def setPlugs( self, plugs ) : From bdd69cb152ece5c10a123f5e515cbbcffb11d45f Mon Sep 17 00:00:00 2001 From: John Haddon Date: Wed, 13 Dec 2023 11:23:23 +0000 Subject: [PATCH 2/7] ShufflesPlug : Improve shuffling API - Add `ignoreMissingSource` argument to `shuffle()`, defaulting to `true` (the original behaviour). - Add `shuffleWithDefaultSource()` method, specifying a fallback source value to be used when a source is missing. These both forward to the same internal function, whose signature I didn't consider suitable for the public API, because of the four permutations of `ignoreMissingSource` and `defaultSource`-null-ness only three make sense. --- Changes.md | 3 ++ include/Gaffer/ShufflePlug.h | 17 ++++++++++- include/Gaffer/ShufflePlug.inl | 39 ++++++++++++++++++++++++-- python/GafferTest/ShufflePlugTest.py | 42 ++++++++++++++++++++++++++++ src/GafferModule/ShufflesBinding.cpp | 27 ++++++++++++++---- 5 files changed, 118 insertions(+), 10 deletions(-) diff --git a/Changes.md b/Changes.md index 8e9b6e2a7d4..cc23d27bca1 100644 --- a/Changes.md +++ b/Changes.md @@ -49,6 +49,9 @@ API - GafferTractorTest : Added `tractorAPI()` method which returns a mock API if Tractor is not available. This allows the GafferTractor module to be tested without Tractor being installed. - ParallelAlgo : Added `canCallOnUIThread()` function. - Label : Added `textSelectable` constructor argument. +- ShufflesPlug : + - Added `ignoreMissingSource` argument to `shuffle()`. + - Added `shuffleWithExtraSources()` method. - ShufflePlugValueWidget : Widgets for the `source` and `destination` plugs can now be customised using standard `plugValueWidget:type` metadata. Breaking Changes diff --git a/include/Gaffer/ShufflePlug.h b/include/Gaffer/ShufflePlug.h index 600fa716056..e979b3f80e9 100644 --- a/include/Gaffer/ShufflePlug.h +++ b/include/Gaffer/ShufflePlug.h @@ -96,8 +96,23 @@ class GAFFER_API ShufflesPlug : public ValuePlug /// Shuffles the sources into a destination container. The container type should have a map /// compatible interface with string-compatible keys (eg std::string, IECore::InternedString). + /// If `ignoreMissingSource` is false, then an exception will be thrown if a source is not + /// found. template - T shuffle( const T &sourceContainer ) const; + T shuffle( const T &sourceContainer, bool ignoreMissingSource = true ) const; + /// As above, but using `extraSources` to provide fallback values for sources not + /// found in `sourceContainer`. A special key, `*`, may be included to provide a fallback + /// for _any_ source. + /// > Note : The `additionalSources` container is only searched for exact matches, _not_ + /// > for wildcard matches. + template + T shuffleWithExtraSources( const T &sourceContainer, const T &extraSources, bool ignoreMissingSource = true ) const; + + private : + + template + T shuffleInternal( const T &sourceContainer, const T *extraSources, bool ignoreMissingSource ) const; + }; } // namespace Gaffer diff --git a/include/Gaffer/ShufflePlug.inl b/include/Gaffer/ShufflePlug.inl index e4dea6be450..ab0a7d16719 100644 --- a/include/Gaffer/ShufflePlug.inl +++ b/include/Gaffer/ShufflePlug.inl @@ -59,7 +59,19 @@ namespace Gaffer { template -T ShufflesPlug::shuffle( const T &sourceContainer ) const +T ShufflesPlug::shuffle( const T &sourceContainer, bool ignoreMissingSource ) const +{ + return shuffleInternal( sourceContainer, static_cast( nullptr ), ignoreMissingSource ); +} + +template +T ShufflesPlug::shuffleWithExtraSources( const T &sourceContainer, const T &extraSources, bool ignoreMissingSource ) const +{ + return shuffleInternal( sourceContainer, &extraSources, ignoreMissingSource ); +} + +template +T ShufflesPlug::shuffleInternal( const T &sourceContainer, const T *extraSources, bool ignoreMissingSource ) const { using NameContainer = std::unordered_set< typename T::key_type >; @@ -103,8 +115,26 @@ T ShufflesPlug::shuffle( const T &sourceContainer ) const // NOTE : No wildcards in source so shuffle is one move. const std::string &srcName = srcPattern; - const typename T::const_iterator sIt = sourceContainer.find( srcName ); + const typename T::mapped_type *srcValue = nullptr; + typename T::const_iterator sIt = sourceContainer.find( srcName ); if( sIt != sourceContainer.end() ) + { + srcValue = &sIt->second; + } + else if( extraSources ) + { + sIt = extraSources->find( srcName ); + if( sIt == extraSources->end() ) + { + sIt = extraSources->find( "*" ); + } + if( sIt != extraSources->end() ) + { + srcValue = &sIt->second; + } + } + + if( srcValue ) { Gaffer::Context::EditableScope scope( Gaffer::Context::current() ); scope.set( g_sourceVariable, &srcName ); @@ -116,7 +146,7 @@ T ShufflesPlug::shuffle( const T &sourceContainer ) const { if( dstReplace || ( destinationContainer.find( dstName ) == destinationContainer.end() ) ) { - destinationContainer[ dstName ] = sIt->second; + destinationContainer[ dstName ] = *srcValue; names.insert( dstName ); } @@ -126,6 +156,9 @@ T ShufflesPlug::shuffle( const T &sourceContainer ) const } } } + } else if( !ignoreMissingSource ) + { + throw IECore::Exception( fmt::format( "Source \"{}\" does not exist", srcName ) ); } } else diff --git a/python/GafferTest/ShufflePlugTest.py b/python/GafferTest/ShufflePlugTest.py index 12577f48c34..99b13004768 100644 --- a/python/GafferTest/ShufflePlugTest.py +++ b/python/GafferTest/ShufflePlugTest.py @@ -484,5 +484,47 @@ def testIdentityNoDeleteSource( self ) : dest = p.shuffle( source ) self.assertEqual( dest, source ) + def testIgnoreMissingSource( self ) : + + plug = Gaffer.ShufflesPlug() + plug.addChild( Gaffer.ShufflePlug( source = "foo", destination = "bar" ) ) + + source = IECore.CompoundData() + self.assertEqual( plug.shuffle( source ), IECore.CompoundData() ) + self.assertEqual( plug.shuffle( source, ignoreMissingSource = True ), IECore.CompoundData() ) + with self.assertRaisesRegex( RuntimeError, "Source \"foo\" does not exist" ) : + plug.shuffle( source, ignoreMissingSource = False ) + + # Wildcards that don't match anything are not considered to be missing. + plug[0]["source"].setValue( "foo*" ) + self.assertEqual( plug.shuffle( source ), IECore.CompoundData() ) + self.assertEqual( plug.shuffle( source, ignoreMissingSource = True ), IECore.CompoundData() ) + self.assertEqual( plug.shuffle( source, ignoreMissingSource = False ), IECore.CompoundData() ) + + def testExtraSources( self ) : + + plug = Gaffer.ShufflesPlug() + plug.addChild( Gaffer.ShufflePlug( source = "foo", destination = "bar" ) ) + + source = IECore.CompoundData() + extraSources = IECore.CompoundData( { "foo" : 10 } ) + self.assertEqual( plug.shuffleWithExtraSources( source, extraSources ), IECore.CompoundData( { "bar" : 10 } ) ) + + # Wildcards are not looked up in `extraSources`. + plug[0]["source"].setValue( "foo*" ) + self.assertEqual( plug.shuffleWithExtraSources( source, extraSources ), IECore.CompoundData() ) + + # `ignoreMissingSource` has the same meaning as for `shuffle()` + plug[0]["source"].setValue( "toto" ) + self.assertEqual( plug.shuffleWithExtraSources( source, extraSources ), IECore.CompoundData() ) + self.assertEqual( plug.shuffleWithExtraSources( source, extraSources, ignoreMissingSource = True ), IECore.CompoundData() ) + with self.assertRaisesRegex( RuntimeError, "Source \"toto\" does not exist" ) : + plug.shuffleWithExtraSources( source, extraSources, ignoreMissingSource = False ) + + # An extra source of `*` matches anything + extraSources["*"] = 20 + self.assertEqual( plug.shuffleWithExtraSources( source, extraSources ), IECore.CompoundData( { "bar" : 20 } ) ) + self.assertEqual( plug.shuffleWithExtraSources( source, extraSources, ignoreMissingSource = False ), IECore.CompoundData( { "bar" : 20 } ) ) + if __name__ == "__main__": unittest.main() diff --git a/src/GafferModule/ShufflesBinding.cpp b/src/GafferModule/ShufflesBinding.cpp index 3c2e200130a..b5a2cee5e66 100644 --- a/src/GafferModule/ShufflesBinding.cpp +++ b/src/GafferModule/ShufflesBinding.cpp @@ -52,19 +52,32 @@ using namespace GafferBindings; namespace { -CompoundObjectPtr shuffleCompoundObject( const ShufflesPlug &shufflesPlug, CompoundObject &source ) +CompoundObjectPtr shuffleCompoundObject( const ShufflesPlug &shufflesPlug, CompoundObject &source, bool ignoreMissingSource ) { IECorePython::ScopedGILRelease gilRelease; + CompoundObjectPtr result = new CompoundObject; + result->members() = shufflesPlug.shuffle( source.members(), ignoreMissingSource ); + return result; +} +CompoundObjectPtr shuffleCompoundObjectWithExtraSources( const ShufflesPlug &shufflesPlug, CompoundObject &source, CompoundObject &extraSources, bool ignoreMissingSource ) +{ + IECorePython::ScopedGILRelease gilRelease; CompoundObjectPtr result = new CompoundObject; - result->members() = shufflesPlug.shuffle( source.members() ); + result->members() = shufflesPlug.shuffleWithExtraSources( source.members(), extraSources.members(), ignoreMissingSource ); return result; } -CompoundDataPtr shuffleCompoundData( const ShufflesPlug &shufflesPlug, CompoundData &source ) +CompoundDataPtr shuffleCompoundData( const ShufflesPlug &shufflesPlug, CompoundData &source, bool ignoreMissingSource ) +{ + IECorePython::ScopedGILRelease gilRelease; + return new CompoundData( shufflesPlug.shuffle( source.readable(), ignoreMissingSource ) ); +} + +CompoundDataPtr shuffleCompoundDataWithExtraSources( const ShufflesPlug &shufflesPlug, CompoundData &source, CompoundData &extraSources, bool ignoreMissingSource ) { IECorePython::ScopedGILRelease gilRelease; - return new CompoundData( shufflesPlug.shuffle( source.readable() ) ); + return new CompoundData( shufflesPlug.shuffleWithExtraSources( source.readable(), extraSources.readable(), ignoreMissingSource ) ); } class ShufflePlugSerialiser : public ValuePlugSerialiser @@ -116,8 +129,10 @@ void GafferModule::bindShuffles() ) ) ) - .def( "shuffle", &shuffleCompoundObject, ( arg( "sourceContainer" ) ) ) - .def( "shuffle", &shuffleCompoundData, ( arg( "sourceContainer" ) ) ) + .def( "shuffle", &shuffleCompoundObject, ( arg( "sourceContainer" ), arg( "ignoreMissingSource" ) = true ) ) + .def( "shuffle", &shuffleCompoundData, ( arg( "sourceContainer" ), arg( "ignoreMissingSource" ) = true ) ) + .def( "shuffleWithExtraSources", &shuffleCompoundObjectWithExtraSources, ( arg( "sourceContainer" ), arg( "extraSources" ), arg( "ignoreMissingSource" ) = true ) ) + .def( "shuffleWithExtraSources", &shuffleCompoundDataWithExtraSources, ( arg( "sourceContainer" ), arg( "extraSources" ), arg( "ignoreMissingSource" ) = true ) ) ; } From 9b1f7da3cef47064e15eb763e628911215517246 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Wed, 13 Dec 2023 11:24:11 +0000 Subject: [PATCH 3/7] Shuffle : Reimplement using ShufflesPlug This brings it into line with ShuffleAttributes and ShufflePrimitiveVariables, adding a fair bit of functionality over what we had before. --- Changes.md | 9 + include/GafferImage/Shuffle.h | 46 +---- include/GafferImage/TypeIds.h | 2 +- python/GafferImageTest/ShuffleTest.py | 147 +++++++++++++- python/GafferImageUI/ShuffleUI.py | 64 +----- src/GafferImage/Shuffle.cpp | 189 ++++++++++-------- .../ImageProcessorBinding.cpp | 18 +- startup/GafferImage/shuffleCompatibility.py | 80 ++++++++ startup/gui/menus.py | 2 +- 9 files changed, 349 insertions(+), 208 deletions(-) create mode 100644 startup/GafferImage/shuffleCompatibility.py diff --git a/Changes.md b/Changes.md index cc23d27bca1..8dbcf8db917 100644 --- a/Changes.md +++ b/Changes.md @@ -15,6 +15,11 @@ Improvements - Cache : Increased default computation cache size to 8Gb. Call `Gaffer.ValuePlug.setCacheMemoryLimit()` from a startup file to override this. - Dispatcher : Reduced internal overhead of `dispatch()` call, with one benchmark showing around a 3x speedup. - ScriptWindow : Added "Save" option to dialogue shown when closing a window containing unsaved changes. +- Shuffle : Reimplemented to match ShuffleAttributes and ShufflePrimitiveVariables. + - Any number of shuffles can be added using the UI. + - Wildcards can be used to match multiple source channels, and expressions can be used to map them to destination channels. + - Source channels can optionally be deleted after shuffling. + - Overwriting of destination channels can optionally be avoided. Fixes ----- @@ -82,6 +87,10 @@ Breaking Changes - OpenColorIOContext : Removed `configEnabledPlug()`, `configValuePlug()`, `workingSpaceEnabledPlug()` and `workingSpaceValuePlug()` methods. Use the OptionalValuePlug child accessors instead. - Windows launch script : Removed the hardcoded `/debugexe` switch used when `GAFFER_DEBUG` is enabled, making it possible to use debuggers other than Visual Studio. Debug switches can be added to the `GAFFER_DEBUGGER` environment variable instead. - Enums : Replaced `IECore.Enum` types with standard Python types from the `enum` module. +- Shuffle : + - Removed ChannelPlug type. Use `Gaffer.ShufflePlug` instead. + - Renamed `channels` plug to `shuffles` plug, matching nodes such as ShuffleAttributes and ShufflePrimitiveVariables. +- ShuffleUI : Removed `nodeMenuCreateCommand()`. Build ----- diff --git a/include/GafferImage/Shuffle.h b/include/GafferImage/Shuffle.h index 7da5337d072..0256497d549 100644 --- a/include/GafferImage/Shuffle.h +++ b/include/GafferImage/Shuffle.h @@ -38,12 +38,11 @@ #include "GafferImage/ImageProcessor.h" -#include "Gaffer/StringPlug.h" +#include "Gaffer/ShufflePlug.h" namespace GafferImage { -/// \todo: Refactor using Gaffer::ShufflesPlug class GAFFERIMAGE_API Shuffle : public ImageProcessor { @@ -54,46 +53,16 @@ class GAFFERIMAGE_API Shuffle : public ImageProcessor GAFFER_NODE_DECLARE_TYPE( GafferImage::Shuffle, ShuffleTypeId, ImageProcessor ); - /// A custom plug to hold the name of an output channel and the - /// name of an input channel to shuffle into it. Add instances - /// of these to the Shuffle::channelsPlug() to define the shuffle. - class GAFFERIMAGE_API ChannelPlug : public Gaffer::ValuePlug - { - - public : - - GAFFER_PLUG_DECLARE_TYPE( GafferImage::Shuffle::ChannelPlug, ShuffleChannelPlugTypeId, Gaffer::ValuePlug ); - - // Standard constructor. This is needed for serialisation. - ChannelPlug( - const std::string &name = defaultName(), - Direction direction=In, - unsigned flags = Default - ); - // Convenience constructor defining a shuffle of the specified - // in channel to the specified out channel. - ChannelPlug( const std::string &out, const std::string &in ); - - Gaffer::StringPlug *outPlug(); - const Gaffer::StringPlug *outPlug() const; - - Gaffer::StringPlug *inPlug(); - const Gaffer::StringPlug *inPlug() const; - - bool acceptsChild( const Gaffer::GraphComponent *potentialChild ) const override; - Gaffer::PlugPtr createCounterpart( const std::string &name, Direction direction ) const override; - - }; - - IE_CORE_DECLAREPTR( ChannelPlug ) - - Gaffer::ValuePlug *channelsPlug(); - const Gaffer::ValuePlug *channelsPlug() const; + Gaffer::ShufflesPlug *shufflesPlug(); + const Gaffer::ShufflesPlug *shufflesPlug() const; void affects( const Gaffer::Plug *input, AffectedPlugsContainer &outputs ) const override; protected : + void hash( const Gaffer::ValuePlug *output, const Gaffer::Context *context, IECore::MurmurHash &h ) const override; + void compute( Gaffer::ValuePlug *output, const Gaffer::Context *context ) const override; + void hashChannelNames( const GafferImage::ImagePlug *parent, const Gaffer::Context *context, IECore::MurmurHash &h ) const override; void hashChannelData( const GafferImage::ImagePlug *parent, const Gaffer::Context *context, IECore::MurmurHash &h ) const override; @@ -102,6 +71,9 @@ class GAFFERIMAGE_API Shuffle : public ImageProcessor private : + Gaffer::ObjectPlug *mappingPlug(); + const Gaffer::ObjectPlug *mappingPlug() const; + std::string inChannelName( const std::string &outChannelName ) const; static size_t g_firstPlugIndex; diff --git a/include/GafferImage/TypeIds.h b/include/GafferImage/TypeIds.h index 1136ed5120c..da921865334 100644 --- a/include/GafferImage/TypeIds.h +++ b/include/GafferImage/TypeIds.h @@ -58,7 +58,7 @@ enum TypeId GradeTypeId = 110763, ShuffleTypeId = 110764, ConstantTypeId = 110765, - ShuffleChannelPlugTypeId = 110766, + ShuffleChannelPlugTypeId = 110766, // Obsolete - available for reuse ChannelMaskPlugTypeId = 110767, WarpTypeId = 110768, VectorWarpTypeId = 110769, diff --git a/python/GafferImageTest/ShuffleTest.py b/python/GafferImageTest/ShuffleTest.py index 349cfba97b3..7b5ff2ec425 100644 --- a/python/GafferImageTest/ShuffleTest.py +++ b/python/GafferImageTest/ShuffleTest.py @@ -133,22 +133,17 @@ def testSerialisation( self ) : self.assertEqual( s3["shuffle"]["channels"][2]["out"].getValue(), "B" ) self.assertEqual( s3["shuffle"]["channels"][2]["in"].getValue(), "R" ) - def testCreateCounterpart( self ) : - - p = GafferImage.Shuffle.ChannelPlug() - p2 = p.createCounterpart( "p2", p.Direction.Out ) - self.assertTrue( isinstance( p2, GafferImage.Shuffle.ChannelPlug ) ) - self.assertTrue( p2.direction(), p.Direction.Out ) - def testAffects( self ) : s = GafferImage.Shuffle() self.assertEqual( s.affects( s["in"]["channelData"] ), [ s["out"]["channelData" ] ] ) - self.assertEqual( s.affects( s["in"]["channelNames"] ), [ s["out"]["channelNames" ] ] ) + self.assertEqual( s.affects( s["in"]["channelNames"] ), [ s["__mapping" ] ] ) s["channels"].addChild( s.ChannelPlug( "R", "G" ) ) - self.assertEqual( s.affects( s["channels"][0]["out"] ), [ s["out"]["channelNames"], s["out"]["channelData"] ] ) + self.assertEqual( s.affects( s["channels"][0]["out"] ), [ s["__mapping"] ] ) + + self.assertEqual( s.affects( s["__mapping"] ), [ s["out"]["channelNames"], s["out"]["channelData" ] ] ) def testMissingInputChannel( self ) : @@ -238,8 +233,142 @@ def testDeep( self ) : self.assertImagesEqual( postFlatten["out"], flatPremult["out"], maxDifference = 0.000001 ) + def testWildCards( self ) : + + constant = GafferImage.Constant() + constant["color"].setValue( imath.Color4f( 0, 1, 2, 3 ) ) + + shuffle = GafferImage.Shuffle() + shuffle["in"].setInput( constant["out"] ) + + self.assertImagesEqual( shuffle["out"], constant["out"] ) + + shuffle["shuffles"].addChild( + Gaffer.ShufflePlug( "[RGB]", "newLayer.${source}" ) + ) + + self.assertEqual( + shuffle["out"].channelNames(), + IECore.StringVectorData( [ "R", "G", "B", "A", "newLayer.R", "newLayer.G", "newLayer.B" ] ) + ) + + for channel in "RGB" : + self.assertEqual( + shuffle["out"].channelData( f"newLayer.{channel}", imath.V2i( 0 ) ), + constant["out"].channelData( channel, imath.V2i( 0 ) ), + ) + + def testWildCardsDontMatchSpecialChannels( self ) : + + constant = GafferImage.Constant() + constant["color"].setValue( imath.Color4f( 0, 1, 2, 3 ) ) + + shuffle = GafferImage.Shuffle() + shuffle["in"].setInput( constant["out"] ) + + self.assertImagesEqual( shuffle["out"], constant["out"] ) + + shuffle["shuffles"].addChild( + Gaffer.ShufflePlug( "*", "newLayer.${source}" ) + ) + + self.assertEqual( + shuffle["out"].channelNames(), + IECore.StringVectorData( [ "R", "G", "B", "A", "newLayer.R", "newLayer.G", "newLayer.B", "newLayer.A" ] ) + ) + + for channel in "RGBA" : + self.assertEqual( + shuffle["out"].channelData( f"newLayer.{channel}", imath.V2i( 0 ) ), + constant["out"].channelData( channel, imath.V2i( 0 ) ), + ) + + def testDeleteSource( self ) : + + constant = GafferImage.Constant() + constant["color"].setValue( imath.Color4f( 0, 1, 2, 3 ) ) + + shuffle = GafferImage.Shuffle() + shuffle["in"].setInput( constant["out"] ) + + self.assertImagesEqual( shuffle["out"], constant["out"] ) + + shuffle["shuffles"].addChild( + Gaffer.ShufflePlug( "R", "newLayer.R" ) + ) + # With `deleteSource` off. + self.assertFalse( shuffle["shuffles"][0]["deleteSource"].getValue() ) + + self.assertEqual( + shuffle["out"].channelNames(), + IECore.StringVectorData( [ "R", "G", "B", "A", "newLayer.R" ] ) + ) + + self.assertEqual( + shuffle["out"].channelData( "newLayer.R", imath.V2i( 0 ) ), + constant["out"].channelData( "R", imath.V2i( 0 ) ), + ) + + # With `deleteSource` on. + + shuffle["shuffles"][0]["deleteSource"].setValue( True ) + + self.assertEqual( + shuffle["out"].channelNames(), + IECore.StringVectorData( [ "G", "B", "A", "newLayer.R" ] ) + ) + + self.assertEqual( + shuffle["out"].channelData( "newLayer.R", imath.V2i( 0 ) ), + constant["out"].channelData( "R", imath.V2i( 0 ) ), + ) + + with self.assertRaisesRegex( Gaffer.ProcessException, "Invalid output channel" ) : + shuffle["out"].channelData( "R", imath.V2i( 0 ) ) + + def testReplaceDestination( self ) : + + constant = GafferImage.Constant() + constant["color"].setValue( imath.Color4f( 0, 1, 2, 3 ) ) + + shuffle = GafferImage.Shuffle() + shuffle["in"].setInput( constant["out"] ) + + self.assertImagesEqual( shuffle["out"], constant["out"] ) + + shuffle["shuffles"].addChild( + Gaffer.ShufflePlug( "R", "G" ) + ) + + # With `replaceDestination` on. + + self.assertTrue( shuffle["shuffles"][0]["replaceDestination"].getValue() ) + + self.assertEqual( + shuffle["out"].channelNames(), + IECore.StringVectorData( [ "R", "G", "B", "A" ] ) + ) + + self.assertEqual( + shuffle["out"].channelData( "G", imath.V2i( 0 ) ), + constant["out"].channelData( "R", imath.V2i( 0 ) ), + ) + + # With `replaceDestination` on. + + shuffle["shuffles"][0]["replaceDestination"].setValue( False ) + + self.assertEqual( + shuffle["out"].channelNames(), + IECore.StringVectorData( [ "R", "G", "B", "A" ] ) + ) + + self.assertEqual( + shuffle["out"].channelData( "G", imath.V2i( 0 ) ), + constant["out"].channelData( "G", imath.V2i( 0 ) ), + ) if __name__ == "__main__": unittest.main() diff --git a/python/GafferImageUI/ShuffleUI.py b/python/GafferImageUI/ShuffleUI.py index d9d845ab3b8..2a0d202ed25 100644 --- a/python/GafferImageUI/ShuffleUI.py +++ b/python/GafferImageUI/ShuffleUI.py @@ -61,28 +61,18 @@ plugs = { - "channels" : [ + "shuffles" : [ "description", """ The definition of the shuffling to be performed - an arbitrary number of channel edits can be made by adding - Shuffle.ChannelPlugs as children of this plug. + ShufflePlugs as children of this plug. """, - "plugValueWidget:type", "GafferUI.LayoutPlugValueWidget", - - ], - - "channels.*.out" : [ - - - "plugValueWidget:type", "GafferImageUI.ChannelPlugValueWidget", - "channelPlugValueWidget:allowNewChannels", True, - ], - "channels.*.in" : [ + "shuffles.*.source" : [ "plugValueWidget:type", "GafferImageUI.ChannelPlugValueWidget", "channelPlugValueWidget:extraChannels", IECore.StringVectorData( [ "__white", "__black" ] ), @@ -90,50 +80,14 @@ ], - } - -) - -def nodeMenuCreateCommand() : - - result = GafferImage.Shuffle() - for channel in ( "R", "G", "B", "A" ) : - result["channels"].addChild( result.ChannelPlug( channel, channel ) ) - - return result - -class _ShuffleChannelPlugValueWidget( GafferUI.PlugValueWidget ) : + "shuffles.*.destination" : [ - def __init__( self, plug, **kw ) : - self.__row = GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) - GafferUI.PlugValueWidget.__init__( self, self.__row, plug, **kw ) - - with self.__row : - - GafferUI.PlugValueWidget.create( plug["out"] ) - - GafferUI.Image( "shuffleArrow.png" ) - - GafferUI.PlugValueWidget.create( plug["in"] ) - - def setPlug( self, plug ) : - - GafferUI.PlugValueWidget.setPlug( self, plug ) - - self.__row[0].setPlug( plug[0] ) - self.__row[2].setPlug( plug[1] ) - - def childPlugValueWidget( self, childPlug ) : - - for w in self.__row[0], self.__row[2] : - if childPlug.isSame( w.getPlug() ) : - return w - - return None + "plugValueWidget:type", "GafferImageUI.ChannelPlugValueWidget", + "channelPlugValueWidget:allowNewChannels", True, - def hasLabel( self ) : + ], - return True + } -GafferUI.PlugValueWidget.registerType( GafferImage.Shuffle.ChannelPlug, _ShuffleChannelPlugValueWidget ) +) \ No newline at end of file diff --git a/src/GafferImage/Shuffle.cpp b/src/GafferImage/Shuffle.cpp index c84baa19f25..617017a6cd4 100644 --- a/src/GafferImage/Shuffle.cpp +++ b/src/GafferImage/Shuffle.cpp @@ -38,67 +38,68 @@ #include "GafferImage/ImageAlgo.h" +#include "IECore/NullObject.h" + +#include + using namespace std; using namespace IECore; using namespace Gaffer; using namespace GafferImage; -////////////////////////////////////////////////////////////////////////// -// Shuffle::ChannelPlug -////////////////////////////////////////////////////////////////////////// - -GAFFER_PLUG_DEFINE_TYPE( Shuffle::ChannelPlug ); - -Shuffle::ChannelPlug::ChannelPlug( const std::string &name, Direction direction, unsigned flags) - : ValuePlug( name, direction, flags ) +namespace { - const unsigned childFlags = flags & ~Dynamic; - addChild( new StringPlug( "out", direction, "", childFlags ) ); - addChild( new StringPlug( "in", direction, "", childFlags ) ); -} -Shuffle::ChannelPlug::ChannelPlug( const std::string &out, const std::string &in ) - : ValuePlug( "channel", In, Default | Dynamic ) +struct MappingData : public IECore::Data { - addChild( new StringPlug( "out" ) ); - addChild( new StringPlug( "in" ) ); - outPlug()->setValue( out ); - inPlug()->setValue( in ); -} -StringPlug *Shuffle::ChannelPlug::outPlug() -{ - return getChild( 0 ); -} + MappingData( const StringVectorData *inChannelNames, const ShufflesPlug *shuffles ) + { + for( const auto &channelName : inChannelNames->readable() ) + { + m_mapping[channelName] = channelName; + } -const StringPlug *Shuffle::ChannelPlug::outPlug() const -{ - return getChild( 0 ); -} + Map extraSources = { + { "__white", "__white" }, + { "__black", "__black" }, + { "*", "__black" } + }; -StringPlug *Shuffle::ChannelPlug::inPlug() -{ - return getChild( 1 ); -} + m_mapping = shuffles->shuffleWithExtraSources( m_mapping, extraSources ); -const StringPlug *Shuffle::ChannelPlug::inPlug() const -{ - return getChild( 1 ); -} + m_outChannelNames = new StringVectorData(); + for( const auto &m : m_mapping ) + { + m_outChannelNames->writable().push_back( m.first ); + } + m_outChannelNames->writable() = ImageAlgo::sortedChannelNames( m_outChannelNames->readable() ); + } -bool Shuffle::ChannelPlug::acceptsChild( const Gaffer::GraphComponent *potentialChild ) const -{ - return children().size() < 2; -} + const StringVectorData *outChannelNames() const { return m_outChannelNames.get(); } -PlugPtr Shuffle::ChannelPlug::createCounterpart( const std::string &name, Direction direction ) const -{ - return new ChannelPlug( name, direction, getFlags() ); -} + const string &inChannelName( const string &outChannelName ) const + { + auto it = m_mapping.find( outChannelName ); + if( it == m_mapping.end() ) + { + throw IECore::Exception( fmt::format( "Invalid output channel {}", outChannelName ) ); + } + return it->second; + } -////////////////////////////////////////////////////////////////////////// -// Shuffle -////////////////////////////////////////////////////////////////////////// + private : + + StringVectorDataPtr m_outChannelNames; + + using Map = unordered_map; + Map m_mapping; + +}; + +IE_CORE_DECLAREPTR( MappingData ) + +} // namespace size_t Shuffle::g_firstPlugIndex = 0; @@ -108,7 +109,8 @@ Shuffle::Shuffle( const std::string &name ) : ImageProcessor( name ) { storeIndexOfNextChild( g_firstPlugIndex ); - addChild( new ValuePlug( "channels" ) ); + addChild( new ShufflesPlug( "shuffles" ) ); + addChild( new ObjectPlug( "__mapping", Plug::Out, IECore::NullObject::defaultNullObject() ) ); // Pass-through the things we don't want to modify. outPlug()->viewNamesPlug()->setInput( inPlug()->viewNamesPlug() ); @@ -123,59 +125,84 @@ Shuffle::~Shuffle() { } -ValuePlug *Shuffle::channelsPlug() +Gaffer::ShufflesPlug *Shuffle::shufflesPlug() +{ + return getChild( g_firstPlugIndex ); +} + +const Gaffer::ShufflesPlug *Shuffle::shufflesPlug() const +{ + return getChild( g_firstPlugIndex ); +} + +Gaffer::ObjectPlug *Shuffle::mappingPlug() { - return getChild( g_firstPlugIndex ); + return getChild( g_firstPlugIndex + 1 ); } -const ValuePlug *Shuffle::channelsPlug() const +const Gaffer::ObjectPlug *Shuffle::mappingPlug() const { - return getChild( g_firstPlugIndex ); + return getChild( g_firstPlugIndex + 1 ); } void Shuffle::affects( const Gaffer::Plug *input, AffectedPlugsContainer &outputs ) const { ImageProcessor::affects( input, outputs ); - if( input == inPlug()->channelNamesPlug() ) + if( + input == inPlug()->channelNamesPlug() || + shufflesPlug()->isAncestorOf( input ) + ) { - outputs.push_back( outPlug()->channelNamesPlug() ); + outputs.push_back( mappingPlug() ); } - else if( input == inPlug()->channelDataPlug() || input == inPlug()->channelNamesPlug() ) + + if( input == mappingPlug() ) { - outputs.push_back( outPlug()->channelDataPlug() ); + outputs.push_back( outPlug()->channelNamesPlug() ); } - else if( channelsPlug()->isAncestorOf( input ) ) + + if( + input == mappingPlug() || + input == inPlug()->channelDataPlug() + ) { - outputs.push_back( outPlug()->channelNamesPlug() ); outputs.push_back( outPlug()->channelDataPlug() ); } } -void Shuffle::hashChannelNames( const GafferImage::ImagePlug *parent, const Gaffer::Context *context, IECore::MurmurHash &h ) const +void Shuffle::hash( const Gaffer::ValuePlug *output, const Gaffer::Context *context, IECore::MurmurHash &h ) const { - ImageProcessor::hashChannelNames( parent, context, h ); - inPlug()->channelNamesPlug()->hash( h ); - for( ChannelPlug::Iterator it( channelsPlug() ); !it.done(); ++it ) + ImageProcessor::hash( output, context, h ); + + if( output == mappingPlug() ) { - (*it)->outPlug()->hash( h ); + inPlug()->channelNamesPlug()->hash( h ); + shufflesPlug()->hash( h ); } } -IECore::ConstStringVectorDataPtr Shuffle::computeChannelNames( const Gaffer::Context *context, const ImagePlug *parent ) const +void Shuffle::compute( Gaffer::ValuePlug *output, const Gaffer::Context *context ) const { - StringVectorDataPtr resultData = inPlug()->channelNamesPlug()->getValue()->copy(); - vector &result = resultData->writable(); - for( ChannelPlug::Iterator it( channelsPlug() ); !it.done(); ++it ) + if( output == mappingPlug() ) { - string channelName = (*it)->outPlug()->getValue(); - if( channelName != "" && find( result.begin(), result.end(), channelName ) == result.end() ) - { - result.push_back( channelName ); - } + ConstStringVectorDataPtr inChannelNames = inPlug()->channelNamesPlug()->getValue(); + static_cast( output )->setValue( new MappingData( inChannelNames.get(), shufflesPlug() ) ); } - return resultData; + return ImageProcessor::compute( output, context ); +} + +void Shuffle::hashChannelNames( const GafferImage::ImagePlug *parent, const Gaffer::Context *context, IECore::MurmurHash &h ) const +{ + ImageProcessor::hashChannelNames( parent, context, h ); + mappingPlug()->hash( h ); +} + +IECore::ConstStringVectorDataPtr Shuffle::computeChannelNames( const Gaffer::Context *context, const ImagePlug *parent ) const +{ + ConstMappingDataPtr mapping = boost::static_pointer_cast( mappingPlug()->getValue() ); + return mapping->outChannelNames(); } @@ -251,20 +278,6 @@ IECore::ConstFloatVectorDataPtr Shuffle::computeChannelData( const std::string & std::string Shuffle::inChannelName( const std::string &outChannelName ) const { ImagePlug::GlobalScope s( Context::current() ); - for( ChannelPlug::Iterator it( channelsPlug() ); !it.done(); ++it ) - { - if( (*it)->outPlug()->getValue() == outChannelName ) - { - const string inChannelName = (*it)->inPlug()->getValue(); - if( inChannelName == "__white" || ImageAlgo::channelExists( inPlug(), inChannelName ) ) - { - return inChannelName; - } - else - { - return "__black"; - } - } - } - return outChannelName; + ConstMappingDataPtr mapping = boost::static_pointer_cast( mappingPlug()->getValue() ); + return mapping->inChannelName( outChannelName ); } diff --git a/src/GafferImageModule/ImageProcessorBinding.cpp b/src/GafferImageModule/ImageProcessorBinding.cpp index e43a2742487..67307ecb07a 100644 --- a/src/GafferImageModule/ImageProcessorBinding.cpp +++ b/src/GafferImageModule/ImageProcessorBinding.cpp @@ -77,6 +77,7 @@ void GafferImageModule::bindImageProcessor() DependencyNodeClass(); DependencyNodeClass(); DependencyNodeClass(); + DependencyNodeClass(); { scope s = GafferBindings::DependencyNodeClass(); @@ -108,21 +109,4 @@ void GafferImageModule::bindImageProcessor() ; } - { - scope s = DependencyNodeClass(); - - PlugClass() - .def( init( - ( - boost::python::arg_( "name" )=GraphComponent::defaultName(), - boost::python::arg_( "direction" )=Plug::In, - boost::python::arg_( "flags" )=Plug::Default - ) - ) - ) - .def( init() ) - .attr( "__qualname__" ) = "Shuffle.ChannelPlug" - ; - } - } diff --git a/startup/GafferImage/shuffleCompatibility.py b/startup/GafferImage/shuffleCompatibility.py new file mode 100644 index 00000000000..c3cdd213d83 --- /dev/null +++ b/startup/GafferImage/shuffleCompatibility.py @@ -0,0 +1,80 @@ +########################################################################## +# +# Copyright (c) 2023, John Haddon. 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 + +class __ChannelPlug( Gaffer.ShufflePlug ) : + + def __init__( self, *args, **kw ) : + + if ( + len( kw ) == 0 and len( args ) == 2 + and isinstance( args[0], str ) and isinstance( args[1], str ) + ) : + Gaffer.ShufflePlug.__init__( self, args[1], args[0] ) + else : + Gaffer.ShufflePlug.__init__( self, *args, **kw ) + +GafferImage.Shuffle.ChannelPlug = __ChannelPlug + +def __shufflePlugGetItem( originalGetItem ) : + + def getItem( self, key ) : + + if key == "in" : + key = "source" + elif key == "out" : + key = "destination" + + return originalGetItem( self, key ) + + return getItem + +Gaffer.ShufflePlug.__getitem__ = __shufflePlugGetItem( Gaffer.ShufflePlug.__getitem__ ) + +def __shuffleGetItem( originalGetItem ) : + + def getItem( self, key ) : + + if key == "channels" : + key = "shuffles" + + return originalGetItem( self, key ) + + return getItem + +GafferImage.Shuffle.__getitem__ = __shuffleGetItem( GafferImage.Shuffle.__getitem__ ) diff --git a/startup/gui/menus.py b/startup/gui/menus.py index db003944913..07748ee124a 100644 --- a/startup/gui/menus.py +++ b/startup/gui/menus.py @@ -381,7 +381,7 @@ def __lightCreator( nodeName, shaderName, shape ) : nodeMenu.append( "/Image/Transform/Offset", GafferImage.Offset ) nodeMenu.append( "/Image/Transform/Mirror", GafferImage.Mirror ) nodeMenu.append( "/Image/Warp/VectorWarp", GafferImage.VectorWarp ) -nodeMenu.append( "/Image/Channels/Shuffle", GafferImageUI.ShuffleUI.nodeMenuCreateCommand, searchText = "Shuffle" ) +nodeMenu.append( "/Image/Channels/Shuffle", GafferImage.Shuffle, searchText = "Shuffle" ) nodeMenu.append( "/Image/Channels/Copy", GafferImage.CopyChannels, searchText = "CopyChannels" ) nodeMenu.append( "/Image/Channels/Delete", GafferImage.DeleteChannels, searchText = "DeleteChannels" ) nodeMenu.append( "/Image/Channels/Collect", GafferImage.CollectImages, searchText = "CollectImages" ) From c554fa15645da0bd34eacd94182440a2036b9594 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Wed, 13 Dec 2023 12:29:50 +0000 Subject: [PATCH 4/7] Avoid legacy Shuffle API This stuff all worked thanks to `startup/GafferImage/shuffleCompatibility.py`, but we don't want to be relying on that forever. Also add a test that we can load a Shuffle node and its settings from a file saved from Gaffer 1.3.9.0. --- .../GafferArnoldTest/ArnoldTextureBakeTest.py | 4 +- python/GafferImage/BleedFill.py | 2 +- python/GafferImageTest/AnaglyphTest.py | 8 +- python/GafferImageTest/ColorSpaceTest.py | 8 +- python/GafferImageTest/DeepHoldoutTest.py | 21 ++--- python/GafferImageTest/DeepStateTest.py | 2 +- python/GafferImageTest/DilateTest.py | 10 +-- python/GafferImageTest/ErodeTest.py | 10 +-- python/GafferImageTest/FlatToDeepTest.py | 4 +- python/GafferImageTest/GradeTest.py | 6 +- python/GafferImageTest/ImageTestCase.py | 8 +- python/GafferImageTest/ImageWriterTest.py | 14 ++-- python/GafferImageTest/MedianTest.py | 10 +-- python/GafferImageTest/MergeTest.py | 8 +- python/GafferImageTest/MixTest.py | 22 ++--- .../GafferImageTest/OpenImageIOReaderTest.py | 22 ++--- python/GafferImageTest/ShuffleTest.py | 84 +++++++++++-------- .../scripts/shuffle-1.3.9.0.gfr | 47 +++++++++++ python/GafferOSLTest/OSLImageTest.py | 4 +- python/GafferSceneTest/ImageToPointsTest.py | 10 +-- 20 files changed, 177 insertions(+), 127 deletions(-) create mode 100644 python/GafferImageTest/scripts/shuffle-1.3.9.0.gfr diff --git a/python/GafferArnoldTest/ArnoldTextureBakeTest.py b/python/GafferArnoldTest/ArnoldTextureBakeTest.py index a54b7544632..98bce7db405 100644 --- a/python/GafferArnoldTest/ArnoldTextureBakeTest.py +++ b/python/GafferArnoldTest/ArnoldTextureBakeTest.py @@ -231,9 +231,7 @@ def testManyImages( self ): shuffleRef["in"].setInput( resizeRef["out"] ) for layer in [ "beauty", "diffuse" ]: for channel in [ "R", "G", "B" ]: - shuffleRef["channels"].addChild( GafferImage.Shuffle.ChannelPlug() ) - shuffleRef["channels"][-1]["in"].setValue( channel ) - shuffleRef["channels"][-1]["out"].setValue( layer + "." + channel ) + shuffleRef["shuffles"].addChild( Gaffer.ShufflePlug( channel, f"{layer}.{channel}" ) ) differenceMerge = GafferImage.Merge() differenceMerge["in"]["in0"].setInput( aovCollect["out"] ) diff --git a/python/GafferImage/BleedFill.py b/python/GafferImage/BleedFill.py index 3493091d605..94589e7b529 100644 --- a/python/GafferImage/BleedFill.py +++ b/python/GafferImage/BleedFill.py @@ -181,7 +181,7 @@ def __init__(self, name = 'BleedFill' ) : self["__unpremult"]["in"].setInput( self["__restoreDataSize"]["out"] ) self["__resetAlpha"] = GafferImage.Shuffle() - self["__resetAlpha"]["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "A", "__white" ) ) + self["__resetAlpha"]["shuffles"].addChild( Gaffer.ShufflePlug( "__white", "A" ) ) self["__resetAlpha"]["in"].setInput( self["__unpremult"]["out"] ) self["__disableSwitch"] = Gaffer.Switch() diff --git a/python/GafferImageTest/AnaglyphTest.py b/python/GafferImageTest/AnaglyphTest.py index 7d1ae193f2b..7421460328a 100644 --- a/python/GafferImageTest/AnaglyphTest.py +++ b/python/GafferImageTest/AnaglyphTest.py @@ -57,10 +57,10 @@ def test( self ) : left = GafferImage.Shuffle() left["in"].setInput( reader["out"] ) - left["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "R", "customRgba.R" ) ) - left["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "G", "customRgba.G" ) ) - left["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "B", "customRgba.B" ) ) - left["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "A", "customRgba.A" ) ) + left["shuffles"].addChild( Gaffer.ShufflePlug( "customRgba.R", "R" ) ) + left["shuffles"].addChild( Gaffer.ShufflePlug( "customRgba.G", "G" ) ) + left["shuffles"].addChild( Gaffer.ShufflePlug( "customRgba.B", "B" ) ) + left["shuffles"].addChild( Gaffer.ShufflePlug( "customRgba.A", "A" ) ) right = GafferImage.ImageTransform() right["in"].setInput( left["out"] ) diff --git a/python/GafferImageTest/ColorSpaceTest.py b/python/GafferImageTest/ColorSpaceTest.py index 84fb8808905..6591d8d1a51 100644 --- a/python/GafferImageTest/ColorSpaceTest.py +++ b/python/GafferImageTest/ColorSpaceTest.py @@ -261,8 +261,8 @@ def testSingleChannelImage( self ) : s = GafferImage.Shuffle() s["in"].setInput( r["out"] ) - s["channels"].addChild( s.ChannelPlug( "G", "R" ) ) - s["channels"].addChild( s.ChannelPlug( "B", "R" ) ) + s["shuffles"].addChild( Gaffer.ShufflePlug( "R", "G" ) ) + s["shuffles"].addChild( Gaffer.ShufflePlug( "R", "B" ) ) # This test is primarily to check that the ColorSpace node doesn't pull # on non-existent input channels, and can still transform a single-channel @@ -289,10 +289,8 @@ def testUnpremultiplied( self ) : i["fileName"].setValue( self.imagesPath() / "circles.exr" ) shuffleAlpha = GafferImage.Shuffle() - shuffleAlpha["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "channel" ) ) + shuffleAlpha["shuffles"].addChild( Gaffer.ShufflePlug( "R", "A" ) ) shuffleAlpha["in"].setInput( i["out"] ) - shuffleAlpha["channels"]["channel"]["out"].setValue( 'A' ) - shuffleAlpha["channels"]["channel"]["in"].setValue( 'R' ) gradeAlpha = GafferImage.Grade() gradeAlpha["in"].setInput( shuffleAlpha["out"] ) diff --git a/python/GafferImageTest/DeepHoldoutTest.py b/python/GafferImageTest/DeepHoldoutTest.py index 63f0e165fc4..98a8d4c31d5 100644 --- a/python/GafferImageTest/DeepHoldoutTest.py +++ b/python/GafferImageTest/DeepHoldoutTest.py @@ -41,6 +41,7 @@ import IECore +import Gaffer import GafferTest import GafferImage import GafferImageTest @@ -95,10 +96,10 @@ def testBasics( self ): # For a more complex holdout, we can create a comparison manually using shuffles and a DeepMerge preShuffle = GafferImage.Shuffle() preShuffle["in"].setInput( representativeImage["out"] ) - preShuffle["channels"].addChild( preShuffle.ChannelPlug( "holdoutR", "R" ) ) - preShuffle["channels"].addChild( preShuffle.ChannelPlug( "holdoutG", "G" ) ) - preShuffle["channels"].addChild( preShuffle.ChannelPlug( "holdoutB", "B" ) ) - preShuffle["channels"].addChild( preShuffle.ChannelPlug( "holdoutA", "A" ) ) + preShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "holdoutR" ) ) + preShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "G", "holdoutG" ) ) + preShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "B", "holdoutB" ) ) + preShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "A", "holdoutA" ) ) manualHoldoutMerge = GafferImage.DeepMerge() manualHoldoutMerge["in"][0].setInput( preShuffle["out"] ) @@ -109,10 +110,10 @@ def testBasics( self ): postShuffle = GafferImage.Shuffle() postShuffle["in"].setInput( manualHoldoutFlatten["out"] ) - postShuffle["channels"].addChild( postShuffle.ChannelPlug( "R", "holdoutR" ) ) - postShuffle["channels"].addChild( postShuffle.ChannelPlug( "G", "holdoutG" ) ) - postShuffle["channels"].addChild( postShuffle.ChannelPlug( "B", "holdoutB" ) ) - postShuffle["channels"].addChild( postShuffle.ChannelPlug( "A", "holdoutA" ) ) + postShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "holdoutR", "R" ) ) + postShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "holdoutG", "G" ) ) + postShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "holdoutB", "B" ) ) + postShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "holdoutA", "A" ) ) channelCleanup = GafferImage.DeleteChannels() channelCleanup["in"].setInput( postShuffle["out"] ) @@ -169,7 +170,7 @@ def testDirtyPropagation( self ): self.assertIn( "out.channelData", dirtiedPlugs ) del cs[:] - aShuffle["channels"].addChild( bShuffle.ChannelPlug( "Z", "__white" ) ) + aShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "__white", "Z" ) ) dirtiedPlugs = { x[0].relativeName( holdout ) for x in cs } self.assertIn( "__intermediateIn.channelData", dirtiedPlugs ) self.assertIn( "__flattened.channelData", dirtiedPlugs ) @@ -179,7 +180,7 @@ def testDirtyPropagation( self ): self.assertIn( "out.channelNames", dirtiedPlugs ) del cs[:] - bShuffle["channels"].addChild( bShuffle.ChannelPlug( "Z", "__white" ) ) + bShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "__white", "Z" ) ) dirtiedPlugs = { x[0].relativeName( holdout ) for x in cs } self.assertIn( "__flattened.channelData", dirtiedPlugs ) self.assertIn( "out.channelData", dirtiedPlugs ) diff --git a/python/GafferImageTest/DeepStateTest.py b/python/GafferImageTest/DeepStateTest.py index e64e3120702..ddb810e01a9 100644 --- a/python/GafferImageTest/DeepStateTest.py +++ b/python/GafferImageTest/DeepStateTest.py @@ -960,7 +960,7 @@ def testMissingChannels( self ) : referenceNoZBack = GafferImage.Shuffle() referenceNoZBack["in"].setInput( deepMerge["out"] ) - referenceNoZBack["channels"].addChild( referenceNoZBack.ChannelPlug( "ZBack", "Z" ) ) + referenceNoZBack["shuffles"].addChild( Gaffer.ShufflePlug( "Z", "ZBack" ) ) referenceFlatten["in"].setInput( referenceNoZBack["out"] ) diff --git a/python/GafferImageTest/DilateTest.py b/python/GafferImageTest/DilateTest.py index 3c44825dd38..437ca111dbf 100644 --- a/python/GafferImageTest/DilateTest.py +++ b/python/GafferImageTest/DilateTest.py @@ -291,8 +291,8 @@ def testAgainstOIIO( self ): driverShuffle = GafferImage.Shuffle() driverShuffle["in"].setInput( imageReader["out"] ) - driverShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "driver", "R" ) ) - driverShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "driven", "R" ) ) + driverShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "driver" ) ) + driverShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "driven" ) ) driverDelete = GafferImage.DeleteChannels() driverDelete["in"].setInput( driverShuffle["out"] ) @@ -306,8 +306,8 @@ def testAgainstOIIO( self ): refDriverShuffle = GafferImage.Shuffle() refDriverShuffle["in"].setInput( refReader["out"] ) - refDriverShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "driver", "R" ) ) - refDriverShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "driven", "R" ) ) + refDriverShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "driver" ) ) + refDriverShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "driven" ) ) refDriverDelete = GafferImage.DeleteChannels() refDriverDelete["in"].setInput( refDriverShuffle["out"] ) @@ -354,7 +354,7 @@ def testAgainstOIIO( self ): with self.subTest( refFile = ref, driverChannel = channelName ): - for channelPlug in [ i["in"] for i in driverShuffle["channels"].children() + refDriverShuffle["channels"].children() ] + [ driverDilate["masterChannel"] ]: + for channelPlug in [ i["source"] for i in driverShuffle["shuffles"].children() + refDriverShuffle["shuffles"].children() ] + [ driverDilate["masterChannel"] ]: channelPlug.setValue( channelName ) self.assertImagesEqual( driverDilate["out"], refDriverDelete["out"], ignoreMetadata = True ) diff --git a/python/GafferImageTest/ErodeTest.py b/python/GafferImageTest/ErodeTest.py index 895ad6b5ba5..4996854fc19 100644 --- a/python/GafferImageTest/ErodeTest.py +++ b/python/GafferImageTest/ErodeTest.py @@ -247,8 +247,8 @@ def testAgainstOIIO( self ): driverShuffle = GafferImage.Shuffle() driverShuffle["in"].setInput( imageReader["out"] ) - driverShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "driver", "R" ) ) - driverShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "driven", "R" ) ) + driverShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "driver" ) ) + driverShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "driven" ) ) driverDelete = GafferImage.DeleteChannels() driverDelete["in"].setInput( driverShuffle["out"] ) @@ -262,8 +262,8 @@ def testAgainstOIIO( self ): refDriverShuffle = GafferImage.Shuffle() refDriverShuffle["in"].setInput( refReader["out"] ) - refDriverShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "driver", "R" ) ) - refDriverShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "driven", "R" ) ) + refDriverShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "driver" ) ) + refDriverShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "driven" ) ) refDriverDelete = GafferImage.DeleteChannels() refDriverDelete["in"].setInput( refDriverShuffle["out"] ) @@ -309,7 +309,7 @@ def testAgainstOIIO( self ): refReader["fileName"].setValue( self.temporaryDirectory() / ref ) with self.subTest( refFile = ref, driverChannel = channelName ): - for channelPlug in [ i["in"] for i in driverShuffle["channels"].children() + refDriverShuffle["channels"].children() ] + [ driverErode["masterChannel"] ]: + for channelPlug in [ i["source"] for i in driverShuffle["shuffles"].children() + refDriverShuffle["shuffles"].children() ] + [ driverErode["masterChannel"] ]: channelPlug.setValue( channelName ) self.assertImagesEqual( driverErode["out"], refDriverDelete["out"], ignoreMetadata = True ) diff --git a/python/GafferImageTest/FlatToDeepTest.py b/python/GafferImageTest/FlatToDeepTest.py index 93ee70472cd..f75f2c6d096 100644 --- a/python/GafferImageTest/FlatToDeepTest.py +++ b/python/GafferImageTest/FlatToDeepTest.py @@ -54,8 +54,8 @@ def testOverall( self ) : shuffle = GafferImage.Shuffle() shuffle["in"].setInput( constant["out"] ) - shuffle["channels"].addChild( shuffle.ChannelPlug( "Z", "R" ) ) - shuffle["channels"].addChild( shuffle.ChannelPlug( "ZBack", "G" ) ) + shuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "Z" ) ) + shuffle["shuffles"].addChild( Gaffer.ShufflePlug( "G", "ZBack" ) ) shuffle["enabled"].setValue( False ) addDepth = GafferImage.FlatToDeep() diff --git a/python/GafferImageTest/GradeTest.py b/python/GafferImageTest/GradeTest.py index 1022cd3853e..3eb8b5d4ac1 100644 --- a/python/GafferImageTest/GradeTest.py +++ b/python/GafferImageTest/GradeTest.py @@ -175,7 +175,7 @@ def testAllChannels( self ): c["color"].setValue( imath.Color4f( 0.125, 0.25, 0.5, 0.75 ) ) s = GafferImage.Shuffle() - s["channels"].addChild( GafferImage.Shuffle.ChannelPlug( 'customChannel', '__white' ) ) + s["shuffles"].addChild( Gaffer.ShufflePlug( "__white", "customChannel" ) ) s["in"].setInput( c["out"] ) g = GafferImage.Grade() @@ -255,10 +255,8 @@ def testUnpremultiplied( self ) : i["fileName"].setValue( self.checkerFile ) shuffleAlpha = GafferImage.Shuffle() - shuffleAlpha["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "channel" ) ) + shuffleAlpha["shuffles"].addChild( Gaffer.ShufflePlug( "R", "A" ) ) shuffleAlpha["in"].setInput( i["out"] ) - shuffleAlpha["channels"]["channel"]["out"].setValue( 'A' ) - shuffleAlpha["channels"]["channel"]["in"].setValue( 'R' ) gradeAlpha = GafferImage.Grade() gradeAlpha["in"].setInput( shuffleAlpha["out"] ) diff --git a/python/GafferImageTest/ImageTestCase.py b/python/GafferImageTest/ImageTestCase.py index 178bc47d75e..14efb8f0f7a 100644 --- a/python/GafferImageTest/ImageTestCase.py +++ b/python/GafferImageTest/ImageTestCase.py @@ -204,10 +204,10 @@ def channelTestImage( self ) : """ ) channelTestImage["Shuffle"] = GafferImage.Shuffle() - channelTestImage["Shuffle"]["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "Z", "R" ) ) - channelTestImage["Shuffle"]["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "ZBack", "G" ) ) - channelTestImage["Shuffle"]["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "custom", "A" ) ) - channelTestImage["Shuffle"]["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "mask", "B" ) ) + channelTestImage["Shuffle"]["shuffles"].addChild( Gaffer.ShufflePlug( "R", "Z" ) ) + channelTestImage["Shuffle"]["shuffles"].addChild( Gaffer.ShufflePlug( "G", "ZBack" ) ) + channelTestImage["Shuffle"]["shuffles"].addChild( Gaffer.ShufflePlug( "A", "custom" ) ) + channelTestImage["Shuffle"]["shuffles"].addChild( Gaffer.ShufflePlug( "B", "mask" ) ) channelTestImage["Shuffle"]["in"].setInput( channelTestImage["Constant"]["out"] ) channelTestImage["in"].setInput( channelTestImage["Shuffle"]["out"] ) diff --git a/python/GafferImageTest/ImageWriterTest.py b/python/GafferImageTest/ImageWriterTest.py index c6b0cacea20..839e808b881 100644 --- a/python/GafferImageTest/ImageWriterTest.py +++ b/python/GafferImageTest/ImageWriterTest.py @@ -1270,7 +1270,7 @@ def testNonDefaultColorSpace( self ) : extraChannel = GafferImage.Shuffle() extraChannel["in"].setInput( reader["out"] ) - extraChannel["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "Q", "R" ) ) + extraChannel["shuffles"].addChild( Gaffer.ShufflePlug( "R", "Q" ) ) writer = GafferImage.ImageWriter() @@ -1404,10 +1404,10 @@ def testNameMetadata( self ) : shuffle = GafferImage.Shuffle() shuffle["in"].setInput( reader["out"] ) - shuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "R", "customRgba.R" ) ) - shuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "G", "customRgba.G" ) ) - shuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "B", "customRgba.B" ) ) - shuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "A", "customRgba.A" ) ) + shuffle["shuffles"].addChild( Gaffer.ShufflePlug( "customRgba.R", "R" ) ) + shuffle["shuffles"].addChild( Gaffer.ShufflePlug( "customRgba.G", "G" ) ) + shuffle["shuffles"].addChild( Gaffer.ShufflePlug( "customRgba.B", "B" ) ) + shuffle["shuffles"].addChild( Gaffer.ShufflePlug( "customRgba.A", "A" ) ) # Write the image out and assert that it reads in again the same. @@ -1482,9 +1482,7 @@ def testReproduceProductionSamples( self ): shuffleDepth = GafferImage.Shuffle() shuffleDepth["in"].setInput( constant["out"] ) - shuffleDepth["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "channel" ) ) - shuffleDepth["channels"]["channel"]["out"].setValue( 'depth.Z' ) - shuffleDepth["channels"]["channel"]["in"].setValue( 'A' ) + shuffleDepth["shuffles"].addChild( Gaffer.ShufflePlug( "A", "depth.Z" ) ) deleteChannels["in"].setInput( shuffleDepth["out"] ) writer["in"].setInput( deleteChannels["out"] ) diff --git a/python/GafferImageTest/MedianTest.py b/python/GafferImageTest/MedianTest.py index 212f421cefa..293a806530f 100644 --- a/python/GafferImageTest/MedianTest.py +++ b/python/GafferImageTest/MedianTest.py @@ -274,8 +274,8 @@ def testAgainstOIIO( self ): driverShuffle = GafferImage.Shuffle() driverShuffle["in"].setInput( imageReader["out"] ) - driverShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "driver", "R" ) ) - driverShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "driven", "R" ) ) + driverShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "driver" ) ) + driverShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "driven" ) ) driverDelete = GafferImage.DeleteChannels() driverDelete["in"].setInput( driverShuffle["out"] ) @@ -287,8 +287,8 @@ def testAgainstOIIO( self ): refDriverShuffle = GafferImage.Shuffle() refDriverShuffle["in"].setInput( refReader["out"] ) - refDriverShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "driver", "R" ) ) - refDriverShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "driven", "R" ) ) + refDriverShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "driver" ) ) + refDriverShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "driven" ) ) refDriverDelete = GafferImage.DeleteChannels() refDriverDelete["in"].setInput( refDriverShuffle["out"] ) @@ -331,7 +331,7 @@ def testAgainstOIIO( self ): continue with self.subTest( refFile = ref, driverChannel = channelName ): - for channelPlug in [ i["in"] for i in driverShuffle["channels"].children() + refDriverShuffle["channels"].children() ] + [ driverMedian["masterChannel"] ]: + for channelPlug in [ i["source"] for i in driverShuffle["shuffles"].children() + refDriverShuffle["shuffles"].children() ] + [ driverMedian["masterChannel"] ]: channelPlug.setValue( channelName ) self.assertImagesEqual( driverMedian["out"], refDriverDelete["out"], ignoreMetadata = True ) diff --git a/python/GafferImageTest/MergeTest.py b/python/GafferImageTest/MergeTest.py index 349eb1aa510..92d84e19f07 100644 --- a/python/GafferImageTest/MergeTest.py +++ b/python/GafferImageTest/MergeTest.py @@ -811,7 +811,7 @@ def testOnlyAlphaVsOnlyRGBBug( self ) : shuf = GafferImage.Shuffle() shuf["in"].setInput( r["out"] ) - shuf["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "A", "R" ) ) + shuf["shuffles"].addChild( Gaffer.ShufflePlug( "R", "R" ) ) delete = GafferImage.DeleteChannels() delete["in"].setInput( shuf["out"] ) @@ -827,8 +827,8 @@ def testOnlyAlphaVsOnlyRGBBug( self ) : referenceShuf = GafferImage.Shuffle() referenceShuf["in"].setInput( delete["out"] ) - referenceShuf["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "", "__black" ) ) - referenceShuf["channels"][0]["out"].setInput( delete["channels"] ) + referenceShuf["shuffles"].addChild( Gaffer.ShufflePlug( "__black", "" ) ) + referenceShuf["shuffles"][0]["destination"].setInput( delete["channels"] ) # We're comparing two ways of filling in the deleted channels with black - either by # merging with a black image, or by shuffling in black. These should be equivalent. @@ -934,7 +934,7 @@ def mergePerf( self, operation, mismatch ): alphaShuffle = GafferImage.Shuffle() alphaShuffle["in"].setInput( r["out"] ) - alphaShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "A", "R" ) ) + alphaShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "A" ) ) transform = GafferImage.Offset() transform["in"].setInput( alphaShuffle["out"] ) diff --git a/python/GafferImageTest/MixTest.py b/python/GafferImageTest/MixTest.py index fc03fa86d06..96c8148d056 100644 --- a/python/GafferImageTest/MixTest.py +++ b/python/GafferImageTest/MixTest.py @@ -498,7 +498,7 @@ def testDeepMix( self ): offset["offset"].setValue( imath.V2i( 33, -25 ) ) addOffsetMarker = GafferImage.Shuffle() - addOffsetMarker["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "offsetted", "__white" ) ) + addOffsetMarker["shuffles"].addChild( Gaffer.ShufflePlug( "__white", "offsetted" ) ) addOffsetMarker["in"].setInput( offset["out"] ) deepMerge = GafferImage.DeepMerge() @@ -608,14 +608,14 @@ def testFuzzDataWindows( self ): file1["fileName"].setValue( self.checkerNegativeDataWindowPath ) file1Shuffle = GafferImage.Shuffle() - file1Shuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "A", "R" ) ) + file1Shuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "A" ) ) file1Shuffle['in'].setInput( file1['out'] ) file2 = GafferImage.ImageReader() file2["fileName"].setValue( self.checkerPath ) file2Shuffle = GafferImage.Shuffle() - file2Shuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "A", "R" ) ) + file2Shuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "A" ) ) file2Shuffle['in'].setInput( file2['out'] ) largeConstant = GafferImage.Constant() @@ -638,10 +638,10 @@ def testFuzzDataWindows( self ): # Create a network using Merge that should match the result of Mix MaskPromote = GafferImage.Shuffle( "MaskPromote" ) - MaskPromote["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "R", "A" ) ) - MaskPromote["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "G", "A" ) ) - MaskPromote["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "B", "A" ) ) - MaskPromote["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "A", "A" ) ) + MaskPromote["shuffles"].addChild( Gaffer.ShufflePlug( "A", "R" ) ) + MaskPromote["shuffles"].addChild( Gaffer.ShufflePlug( "A", "G" ) ) + MaskPromote["shuffles"].addChild( Gaffer.ShufflePlug( "A", "B" ) ) + MaskPromote["shuffles"].addChild( Gaffer.ShufflePlug( "A", "A" ) ) MaskPromote['in'].setInput( Mix['mask'] ) Input0Mult = GafferImage.Merge( "Input0Mult" ) @@ -666,10 +666,10 @@ def testFuzzDataWindows( self ): # Expand the data window of the reference to match the Mix, so we can compare BlackBackground = GafferImage.Shuffle( "BlackBackground" ) - BlackBackground["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "R", "__black" ) ) - BlackBackground["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "G", "__black" ) ) - BlackBackground["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "B", "__black" ) ) - BlackBackground["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "A", "__black" ) ) + BlackBackground["shuffles"].addChild( Gaffer.ShufflePlug( "__black", "R" ) ) + BlackBackground["shuffles"].addChild( Gaffer.ShufflePlug( "__black", "G" ) ) + BlackBackground["shuffles"].addChild( Gaffer.ShufflePlug( "__black", "B" ) ) + BlackBackground["shuffles"].addChild( Gaffer.ShufflePlug( "__black", "A" ) ) BlackBackground["in"].setInput( Mix["out"] ) ReferenceWithDataWindow = GafferImage.Merge( "ReferenceWithDataWindow" ) diff --git a/python/GafferImageTest/OpenImageIOReaderTest.py b/python/GafferImageTest/OpenImageIOReaderTest.py index 4213ee5ab01..edeef4b5bdc 100644 --- a/python/GafferImageTest/OpenImageIOReaderTest.py +++ b/python/GafferImageTest/OpenImageIOReaderTest.py @@ -638,22 +638,22 @@ def testMultipartRead( self ) : set([ "customRgba.R", "customRgba.G", "customRgba.B", "customRgba.A", "customRgb.R", "customRgb.G", "customRgb.B", "customDepth.Z" ]) ) - multipartShuffle["channels"].clearChildren() - multipartShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "R", "customRgba.R" ) ) - multipartShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "G", "customRgba.G" ) ) - multipartShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "B", "customRgba.B" ) ) - multipartShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "A", "customRgba.A" ) ) + multipartShuffle["shuffles"].clearChildren() + multipartShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "customRgba.R", "R" ) ) + multipartShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "customRgba.G", "G" ) ) + multipartShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "customRgba.B", "B" ) ) + multipartShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "customRgba.A", "A" ) ) self.assertImagesEqual( compareDelete["out"], multipartDelete["out"], ignoreMetadata = True ) - multipartShuffle["channels"].clearChildren() - multipartShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "R", "customRgb.R" ) ) - multipartShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "G", "customRgb.G" ) ) - multipartShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "B", "customRgb.B" ) ) + multipartShuffle["shuffles"].clearChildren() + multipartShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "customRgb.R", "R" ) ) + multipartShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "customRgb.G", "G" ) ) + multipartShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "customRgb.B", "B" ) ) compareDelete['channels'].setValue( "A" ) self.assertImagesEqual( compareDelete["out"], multipartDelete["out"], ignoreMetadata = True ) - multipartShuffle["channels"].clearChildren() - multipartShuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "G", "customDepth.Z" ) ) + multipartShuffle["shuffles"].clearChildren() + multipartShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "customDepth.Z", "G" ) ) compareDelete['channels'].setValue( "R B A" ) self.assertImagesEqual( compareDelete["out"], multipartDelete["out"], ignoreMetadata = True ) diff --git a/python/GafferImageTest/ShuffleTest.py b/python/GafferImageTest/ShuffleTest.py index 7b5ff2ec425..d84ba694af2 100644 --- a/python/GafferImageTest/ShuffleTest.py +++ b/python/GafferImageTest/ShuffleTest.py @@ -36,6 +36,7 @@ import unittest import os +import pathlib import imath import IECore @@ -71,10 +72,10 @@ def test( self ) : ) ) - s["channels"].addChild( s.ChannelPlug( "R", "G" ) ) - s["channels"].addChild( s.ChannelPlug( "G", "B" ) ) - s["channels"].addChild( s.ChannelPlug( "B", "A" ) ) - s["channels"].addChild( s.ChannelPlug( "A", "R" ) ) + s["shuffles"].addChild( Gaffer.ShufflePlug( "G", "R" ) ) + s["shuffles"].addChild( Gaffer.ShufflePlug( "B", "G" ) ) + s["shuffles"].addChild( Gaffer.ShufflePlug( "A", "B" ) ) + s["shuffles"].addChild( Gaffer.ShufflePlug( "R", "A" ) ) for outName, inName in [ ( "R", "G" ), ( "G", "B" ), ( "B", "A" ), ( "A", "R" ) ] : self.assertEqual( @@ -92,7 +93,7 @@ def testAddConstantChannel( self ) : s = GafferImage.Shuffle() self.assertEqual( s["out"]["channelNames"].getValue(), IECore.StringVectorData() ) - s["channels"].addChild( s.ChannelPlug( "A", "__white" ) ) + s["shuffles"].addChild( Gaffer.ShufflePlug( "__white", "A" ) ) self.assertEqual( s["out"]["channelNames"].getValue(), IECore.StringVectorData( [ "A" ] ) ) self.assertEqual( s["out"].channelData( "A", imath.V2i( 0 ) )[0], 1 ) @@ -107,31 +108,31 @@ def testSerialisation( self ) : s = Gaffer.ScriptNode() s["shuffle"] = GafferImage.Shuffle() - s["shuffle"]["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "R", "G" ) ) - s["shuffle"]["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "G", "B" ) ) - s["shuffle"]["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "B", "R" ) ) + s["shuffle"]["shuffles"].addChild( Gaffer.ShufflePlug( "G", "R" ) ) + s["shuffle"]["shuffles"].addChild( Gaffer.ShufflePlug( "B", "G" ) ) + s["shuffle"]["shuffles"].addChild( Gaffer.ShufflePlug( "R", "B" ) ) s2 = Gaffer.ScriptNode() s2.execute( s.serialise() ) - self.assertTrue( len( s2["shuffle"]["channels"] ), 3 ) - self.assertEqual( s2["shuffle"]["channels"][0]["out"].getValue(), "R" ) - self.assertEqual( s2["shuffle"]["channels"][0]["in"].getValue(), "G" ) - self.assertEqual( s2["shuffle"]["channels"][1]["out"].getValue(), "G" ) - self.assertEqual( s2["shuffle"]["channels"][1]["in"].getValue(), "B" ) - self.assertEqual( s2["shuffle"]["channels"][2]["out"].getValue(), "B" ) - self.assertEqual( s2["shuffle"]["channels"][2]["in"].getValue(), "R" ) + self.assertTrue( len( s2["shuffle"]["shuffles"] ), 3 ) + self.assertEqual( s2["shuffle"]["shuffles"][0]["destination"].getValue(), "R" ) + self.assertEqual( s2["shuffle"]["shuffles"][0]["source"].getValue(), "G" ) + self.assertEqual( s2["shuffle"]["shuffles"][1]["destination"].getValue(), "G" ) + self.assertEqual( s2["shuffle"]["shuffles"][1]["source"].getValue(), "B" ) + self.assertEqual( s2["shuffle"]["shuffles"][2]["destination"].getValue(), "B" ) + self.assertEqual( s2["shuffle"]["shuffles"][2]["source"].getValue(), "R" ) s3 = Gaffer.ScriptNode() s3.execute( s2.serialise() ) - self.assertTrue( len( s3["shuffle"]["channels"] ), 3 ) - self.assertEqual( s3["shuffle"]["channels"][0]["out"].getValue(), "R" ) - self.assertEqual( s3["shuffle"]["channels"][0]["in"].getValue(), "G" ) - self.assertEqual( s3["shuffle"]["channels"][1]["out"].getValue(), "G" ) - self.assertEqual( s3["shuffle"]["channels"][1]["in"].getValue(), "B" ) - self.assertEqual( s3["shuffle"]["channels"][2]["out"].getValue(), "B" ) - self.assertEqual( s3["shuffle"]["channels"][2]["in"].getValue(), "R" ) + self.assertTrue( len( s3["shuffle"]["shuffles"] ), 3 ) + self.assertEqual( s3["shuffle"]["shuffles"][0]["destination"].getValue(), "R" ) + self.assertEqual( s3["shuffle"]["shuffles"][0]["source"].getValue(), "G" ) + self.assertEqual( s3["shuffle"]["shuffles"][1]["destination"].getValue(), "G" ) + self.assertEqual( s3["shuffle"]["shuffles"][1]["source"].getValue(), "B" ) + self.assertEqual( s3["shuffle"]["shuffles"][2]["destination"].getValue(), "B" ) + self.assertEqual( s3["shuffle"]["shuffles"][2]["source"].getValue(), "R" ) def testAffects( self ) : @@ -140,8 +141,8 @@ def testAffects( self ) : self.assertEqual( s.affects( s["in"]["channelData"] ), [ s["out"]["channelData" ] ] ) self.assertEqual( s.affects( s["in"]["channelNames"] ), [ s["__mapping" ] ] ) - s["channels"].addChild( s.ChannelPlug( "R", "G" ) ) - self.assertEqual( s.affects( s["channels"][0]["out"] ), [ s["__mapping"] ] ) + s["shuffles"].addChild( Gaffer.ShufflePlug( "G", "R" ) ) + self.assertEqual( s.affects( s["shuffles"][0]["out"] ), [ s["__mapping"] ] ) self.assertEqual( s.affects( s["__mapping"] ), [ s["out"]["channelNames"], s["out"]["channelData" ] ] ) @@ -153,10 +154,10 @@ def testMissingInputChannel( self ) : s = GafferImage.Shuffle() s["in"].setInput( r["out"] ) - s["channels"].addChild( s.ChannelPlug( "R", "A" ) ) - s["channels"].addChild( s.ChannelPlug( "G", "B" ) ) - s["channels"].addChild( s.ChannelPlug( "B", "G" ) ) - s["channels"].addChild( s.ChannelPlug( "A", "R" ) ) + s["shuffles"].addChild( Gaffer.ShufflePlug( "A", "R" ) ) + s["shuffles"].addChild( Gaffer.ShufflePlug( "B", "G" ) ) + s["shuffles"].addChild( Gaffer.ShufflePlug( "G", "B" ) ) + s["shuffles"].addChild( Gaffer.ShufflePlug( "R", "A" ) ) black = IECore.FloatVectorData( [ 0 ] * GafferImage.ImagePlug.tileSize() * GafferImage.ImagePlug.tileSize() ) @@ -180,11 +181,11 @@ def testDeep( self ) : flatShuffle = GafferImage.Shuffle() flatShuffle["in"].setInput( preFlatten["out"] ) - flatShuffle["channels"].setInput( deepShuffle["channels"] ) + flatShuffle["shuffles"].setInput( deepShuffle["shuffles"] ) - deepShuffle["channels"].addChild( deepShuffle.ChannelPlug( "R", "B" ) ) - deepShuffle["channels"].addChild( deepShuffle.ChannelPlug( "G", "R" ) ) - deepShuffle["channels"].addChild( deepShuffle.ChannelPlug( "B", "G" ) ) + deepShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "B", "R" ) ) + deepShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "G" ) ) + deepShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "G", "B" ) ) deepOrig = GafferImage.ImageAlgo.tiles( representativeDeep["out"] ) flatOrig = GafferImage.ImageAlgo.tiles( preFlatten["out"] ) @@ -205,10 +206,10 @@ def testDeep( self ) : self.assertImagesEqual( postFlatten["out"], flatShuffle["out"] ) - deepShuffle["channels"].clearChildren() - deepShuffle["channels"].addChild( deepShuffle.ChannelPlug( "R", "__black" ) ) - deepShuffle["channels"].addChild( deepShuffle.ChannelPlug( "G", "__white" ) ) - deepShuffle["channels"].addChild( deepShuffle.ChannelPlug( "B", "__black" ) ) + deepShuffle["shuffles"].clearChildren() + deepShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "__black", "R" ) ) + deepShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "__white", "G" ) ) + deepShuffle["shuffles"].addChild( Gaffer.ShufflePlug( "__black", "B" ) ) flatGreen = GafferImage.ImageAlgo.tiles( flatShuffle["out"] ) deepGreen = GafferImage.ImageAlgo.tiles( deepShuffle["out"] ) @@ -370,5 +371,16 @@ def testReplaceDestination( self ) : constant["out"].channelData( "G", imath.V2i( 0 ) ), ) + def testLoadFrom1_3( self ) : + + script = Gaffer.ScriptNode() + script["fileName"].setValue( pathlib.Path( __file__ ).parent / "scripts" / "shuffle-1.3.9.0.gfr" ) + script.load() + + constant = GafferImage.Constant() + constant["color"].setValue( imath.Color4f( 3, 2, 1, 0 ) ) + + self.assertImagesEqual( script["Shuffle"]["out"], constant["out"] ) + if __name__ == "__main__": unittest.main() diff --git a/python/GafferImageTest/scripts/shuffle-1.3.9.0.gfr b/python/GafferImageTest/scripts/shuffle-1.3.9.0.gfr new file mode 100644 index 00000000000..85c55b5e258 --- /dev/null +++ b/python/GafferImageTest/scripts/shuffle-1.3.9.0.gfr @@ -0,0 +1,47 @@ +import Gaffer +import GafferImage +import imath + +Gaffer.Metadata.registerValue( parent, "serialiser:milestoneVersion", 1, persistent=False ) +Gaffer.Metadata.registerValue( parent, "serialiser:majorVersion", 3, persistent=False ) +Gaffer.Metadata.registerValue( parent, "serialiser:minorVersion", 9, 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["Shuffle"] = GafferImage.Shuffle( "Shuffle" ) +parent.addChild( __children["Shuffle"] ) +__children["Shuffle"]["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "channel", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["Shuffle"]["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "channel1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["Shuffle"]["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "channel2", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["Shuffle"]["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "channel3", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["Shuffle"].addChild( Gaffer.V2fPlug( "__uiPosition", defaultValue = imath.V2f( 0, 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +__children["Constant"] = GafferImage.Constant( "Constant" ) +parent.addChild( __children["Constant"] ) +__children["Constant"].addChild( Gaffer.V2fPlug( "__uiPosition", defaultValue = imath.V2f( 0, 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) ) +parent["variables"]["imageCataloguePort"]["value"].setValue( 57595 ) +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["Shuffle"]["in"].setInput( __children["Constant"]["out"] ) +__children["Shuffle"]["channels"]["channel"]["out"].setValue( 'R' ) +__children["Shuffle"]["channels"]["channel"]["in"].setValue( 'A' ) +__children["Shuffle"]["channels"]["channel1"]["out"].setValue( 'G' ) +__children["Shuffle"]["channels"]["channel1"]["in"].setValue( 'B' ) +__children["Shuffle"]["channels"]["channel2"]["out"].setValue( 'B' ) +__children["Shuffle"]["channels"]["channel2"]["in"].setValue( 'G' ) +__children["Shuffle"]["channels"]["channel3"]["out"].setValue( 'A' ) +__children["Shuffle"]["channels"]["channel3"]["in"].setValue( 'R' ) +__children["Shuffle"]["__uiPosition"].setValue( imath.V2f( -2.89999843, 0.149999589 ) ) +__children["Constant"]["color"].setValue( imath.Color4f( 0, 1, 2, 3 ) ) +__children["Constant"]["__uiPosition"].setValue( imath.V2f( -2.89999843, 8.31406212 ) ) + + +del __children diff --git a/python/GafferOSLTest/OSLImageTest.py b/python/GafferOSLTest/OSLImageTest.py index 1d96f56a6a1..08d54ed24ef 100644 --- a/python/GafferOSLTest/OSLImageTest.py +++ b/python/GafferOSLTest/OSLImageTest.py @@ -77,9 +77,7 @@ def test( self ) : shuffle = GafferImage.Shuffle() shuffle["in"].setInput( reader["out"] ) - shuffle["channels"].addChild( GafferImage.Shuffle.ChannelPlug( "channel" ) ) - shuffle["channels"]["channel"]["out"].setValue( 'unchangedR' ) - shuffle["channels"]["channel"]["in"].setValue( 'R' ) + shuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "unchangedR" ) ) image = GafferOSL.OSLImage() image["in"].setInput( shuffle["out"] ) diff --git a/python/GafferSceneTest/ImageToPointsTest.py b/python/GafferSceneTest/ImageToPointsTest.py index 2ad2ed21c11..72ab028e5c8 100644 --- a/python/GafferSceneTest/ImageToPointsTest.py +++ b/python/GafferSceneTest/ImageToPointsTest.py @@ -144,11 +144,11 @@ def testPrimitiveVariables( self ) : shuffle = GafferImage.Shuffle() shuffle["in"].setInput( ramp["out"] ) - shuffle["channels"].addChild( shuffle.ChannelPlug( "diffuse.R", "B" ) ) - shuffle["channels"].addChild( shuffle.ChannelPlug( "diffuse.G", "G" ) ) - shuffle["channels"].addChild( shuffle.ChannelPlug( "diffuse.B", "R" ) ) - shuffle["channels"].addChild( shuffle.ChannelPlug( "depth", "R" ) ) - shuffle["channels"].addChild( shuffle.ChannelPlug( "layer.specialChannel", "B" ) ) + shuffle["shuffles"].addChild( Gaffer.ShufflePlug( "B", "diffuse.R" ) ) + shuffle["shuffles"].addChild( Gaffer.ShufflePlug( "G", "diffuse.G" ) ) + shuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "diffuse.B" ) ) + shuffle["shuffles"].addChild( Gaffer.ShufflePlug( "R", "depth" ) ) + shuffle["shuffles"].addChild( Gaffer.ShufflePlug( "B", "layer.specialChannel" ) ) imageToPoints = GafferScene.ImageToPoints() imageToPoints["image"].setInput( shuffle["out"] ) From 77a2b46ac1cbfe86a571d54fec62c2b155f6cd3e Mon Sep 17 00:00:00 2001 From: John Haddon Date: Wed, 13 Dec 2023 12:58:00 +0000 Subject: [PATCH 5/7] Shuffle : Add `missingSourceMode` plug Fixes #3976. --- Changes.md | 12 +++--- include/GafferImage/Shuffle.h | 10 +++++ python/GafferImageTest/ShuffleTest.py | 30 +++++++++++++++ python/GafferImageUI/ShuffleUI.py | 22 +++++++++++ src/GafferImage/Shuffle.cpp | 38 ++++++++++++++----- .../ImageProcessorBinding.cpp | 11 +++++- 6 files changed, 107 insertions(+), 16 deletions(-) diff --git a/Changes.md b/Changes.md index 8dbcf8db917..de70b08ef0b 100644 --- a/Changes.md +++ b/Changes.md @@ -15,11 +15,13 @@ Improvements - Cache : Increased default computation cache size to 8Gb. Call `Gaffer.ValuePlug.setCacheMemoryLimit()` from a startup file to override this. - Dispatcher : Reduced internal overhead of `dispatch()` call, with one benchmark showing around a 3x speedup. - ScriptWindow : Added "Save" option to dialogue shown when closing a window containing unsaved changes. -- Shuffle : Reimplemented to match ShuffleAttributes and ShufflePrimitiveVariables. - - Any number of shuffles can be added using the UI. - - Wildcards can be used to match multiple source channels, and expressions can be used to map them to destination channels. - - Source channels can optionally be deleted after shuffling. - - Overwriting of destination channels can optionally be avoided. +- Shuffle : + - Reimplemented to match ShuffleAttributes and ShufflePrimitiveVariables. + - Any number of shuffles can be added using the UI. + - Wildcards can be used to match multiple source channels, and expressions can be used to map them to destination channels. + - Source channels can optionally be deleted after shuffling. + - Overwriting of destination channels can optionally be avoided. + - Added `missingSourceMode` plug to determine behaviour when a source channel doesn't exist. Fixes ----- diff --git a/include/GafferImage/Shuffle.h b/include/GafferImage/Shuffle.h index 0256497d549..4233e8b731b 100644 --- a/include/GafferImage/Shuffle.h +++ b/include/GafferImage/Shuffle.h @@ -53,6 +53,16 @@ class GAFFERIMAGE_API Shuffle : public ImageProcessor GAFFER_NODE_DECLARE_TYPE( GafferImage::Shuffle, ShuffleTypeId, ImageProcessor ); + enum class MissingSourceMode + { + Ignore, + Error, + Black + }; + + Gaffer::IntPlug *missingSourceModePlug(); + const Gaffer::IntPlug *missingSourceModePlug() const; + Gaffer::ShufflesPlug *shufflesPlug(); const Gaffer::ShufflesPlug *shufflesPlug() const; diff --git a/python/GafferImageTest/ShuffleTest.py b/python/GafferImageTest/ShuffleTest.py index d84ba694af2..d68fd205de7 100644 --- a/python/GafferImageTest/ShuffleTest.py +++ b/python/GafferImageTest/ShuffleTest.py @@ -382,5 +382,35 @@ def testLoadFrom1_3( self ) : self.assertImagesEqual( script["Shuffle"]["out"], constant["out"] ) + def testMissingSourceMode( self ) : + + constant = GafferImage.Constant() + constant["color"].setValue( imath.Color4f( 1, 1, 1, 1 ) ) + + shuffle = GafferImage.Shuffle() + shuffle["in"].setInput( constant["out"] ) + shuffle["shuffles"].addChild( Gaffer.ShufflePlug( "nonExistent", "R" ) ) + + self.assertEqual( shuffle["missingSourceMode"].getValue(), shuffle.MissingSourceMode.Black ) + self.assertEqual( shuffle["out"].channelData( "R", imath.V2i( 0 ) )[0], 0 ) + + shuffle["missingSourceMode"].setValue( shuffle.MissingSourceMode.Ignore ) + self.assertEqual( shuffle["out"].channelData( "R", imath.V2i( 0 ) )[0], 1 ) + + shuffle["missingSourceMode"].setValue( shuffle.MissingSourceMode.Error ) + with self.assertRaisesRegex( Gaffer.ProcessException, "Source \"nonExistent\" does not exist" ) : + shuffle["out"].channelData( "R", imath.V2i( 0 ) ) + + def testIgnoreMissingSourceDoesnCreateChannels( self ) : + + constant = GafferImage.Constant() + + shuffle = GafferImage.Shuffle() + shuffle["in"].setInput( constant["out"] ) + shuffle["shuffles"].addChild( Gaffer.ShufflePlug( "nonExistent", "newChannel" ) ) + + shuffle["missingSourceMode"].setValue( shuffle.MissingSourceMode.Ignore ) + self.assertEqual( shuffle["out"].channelNames(), IECore.StringVectorData( [ "R", "G", "B", "A" ] ) ) + if __name__ == "__main__": unittest.main() diff --git a/python/GafferImageUI/ShuffleUI.py b/python/GafferImageUI/ShuffleUI.py index 2a0d202ed25..1484a49ca24 100644 --- a/python/GafferImageUI/ShuffleUI.py +++ b/python/GafferImageUI/ShuffleUI.py @@ -61,6 +61,28 @@ plugs = { + "missingSourceMode" : [ + + "description", + """ + Determines behaviour when the source channel doesn't exist : + + - Ignore : No change is made to the destination channel. + - Error : The node errors. + - Black : Black is shuffled into the destination channel. + + > Note : Does not apply when source contains wildcards. + """, + + "plugValueWidget:type", "GafferUI.PresetsPlugValueWidget", + "preset:Ignore", GafferImage.Shuffle.MissingSourceMode.Ignore, + "preset:Error", GafferImage.Shuffle.MissingSourceMode.Error, + "preset:Black", GafferImage.Shuffle.MissingSourceMode.Black, + + "layout:divider", True, + + ], + "shuffles" : [ "description", diff --git a/src/GafferImage/Shuffle.cpp b/src/GafferImage/Shuffle.cpp index 617017a6cd4..0e39367d9b6 100644 --- a/src/GafferImage/Shuffle.cpp +++ b/src/GafferImage/Shuffle.cpp @@ -53,7 +53,7 @@ namespace struct MappingData : public IECore::Data { - MappingData( const StringVectorData *inChannelNames, const ShufflesPlug *shuffles ) + MappingData( const StringVectorData *inChannelNames, const ShufflesPlug *shuffles, Shuffle::MissingSourceMode mode ) { for( const auto &channelName : inChannelNames->readable() ) { @@ -62,11 +62,14 @@ struct MappingData : public IECore::Data Map extraSources = { { "__white", "__white" }, - { "__black", "__black" }, - { "*", "__black" } + { "__black", "__black" } }; + if( mode == Shuffle::MissingSourceMode::Black ) + { + extraSources["*"] = "__black"; + } - m_mapping = shuffles->shuffleWithExtraSources( m_mapping, extraSources ); + m_mapping = shuffles->shuffleWithExtraSources( m_mapping, extraSources, mode == Shuffle::MissingSourceMode::Ignore ); m_outChannelNames = new StringVectorData(); for( const auto &m : m_mapping ) @@ -109,6 +112,7 @@ Shuffle::Shuffle( const std::string &name ) : ImageProcessor( name ) { storeIndexOfNextChild( g_firstPlugIndex ); + addChild( new IntPlug( "missingSourceMode", Plug::In, (int)MissingSourceMode::Black, (int)MissingSourceMode::Ignore, (int)MissingSourceMode::Black ) ); addChild( new ShufflesPlug( "shuffles" ) ); addChild( new ObjectPlug( "__mapping", Plug::Out, IECore::NullObject::defaultNullObject() ) ); @@ -125,24 +129,34 @@ Shuffle::~Shuffle() { } +Gaffer::IntPlug *Shuffle::missingSourceModePlug() +{ + return getChild( g_firstPlugIndex ); +} + +const Gaffer::IntPlug *Shuffle::missingSourceModePlug() const +{ + return getChild( g_firstPlugIndex ); +} + Gaffer::ShufflesPlug *Shuffle::shufflesPlug() { - return getChild( g_firstPlugIndex ); + return getChild( g_firstPlugIndex +1 ); } const Gaffer::ShufflesPlug *Shuffle::shufflesPlug() const { - return getChild( g_firstPlugIndex ); + return getChild( g_firstPlugIndex + 1 ); } Gaffer::ObjectPlug *Shuffle::mappingPlug() { - return getChild( g_firstPlugIndex + 1 ); + return getChild( g_firstPlugIndex + 2 ); } const Gaffer::ObjectPlug *Shuffle::mappingPlug() const { - return getChild( g_firstPlugIndex + 1 ); + return getChild( g_firstPlugIndex + 2 ); } void Shuffle::affects( const Gaffer::Plug *input, AffectedPlugsContainer &outputs ) const @@ -151,7 +165,8 @@ void Shuffle::affects( const Gaffer::Plug *input, AffectedPlugsContainer &output if( input == inPlug()->channelNamesPlug() || - shufflesPlug()->isAncestorOf( input ) + shufflesPlug()->isAncestorOf( input ) || + input == missingSourceModePlug() ) { outputs.push_back( mappingPlug() ); @@ -179,6 +194,7 @@ void Shuffle::hash( const Gaffer::ValuePlug *output, const Gaffer::Context *cont { inPlug()->channelNamesPlug()->hash( h ); shufflesPlug()->hash( h ); + missingSourceModePlug()->hash( h ); } } @@ -187,7 +203,9 @@ void Shuffle::compute( Gaffer::ValuePlug *output, const Gaffer::Context *context if( output == mappingPlug() ) { ConstStringVectorDataPtr inChannelNames = inPlug()->channelNamesPlug()->getValue(); - static_cast( output )->setValue( new MappingData( inChannelNames.get(), shufflesPlug() ) ); + static_cast( output )->setValue( + new MappingData( inChannelNames.get(), shufflesPlug(), (MissingSourceMode)missingSourceModePlug()->getValue() ) + ); } return ImageProcessor::compute( output, context ); diff --git a/src/GafferImageModule/ImageProcessorBinding.cpp b/src/GafferImageModule/ImageProcessorBinding.cpp index 67307ecb07a..2d0d5614671 100644 --- a/src/GafferImageModule/ImageProcessorBinding.cpp +++ b/src/GafferImageModule/ImageProcessorBinding.cpp @@ -77,7 +77,6 @@ void GafferImageModule::bindImageProcessor() DependencyNodeClass(); DependencyNodeClass(); DependencyNodeClass(); - DependencyNodeClass(); { scope s = GafferBindings::DependencyNodeClass(); @@ -109,4 +108,14 @@ void GafferImageModule::bindImageProcessor() ; } + { + scope s = DependencyNodeClass(); + + enum_( "MissingSourceMode" ) + .value( "Ignore", Shuffle::MissingSourceMode::Ignore ) + .value( "Error", Shuffle::MissingSourceMode::Error ) + .value( "Black", Shuffle::MissingSourceMode::Black ) + ; + } + } From 4977f34fbbb0da43735800345b5ba8ac9d422f66 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Wed, 13 Dec 2023 14:13:15 +0000 Subject: [PATCH 6/7] ChannelPlugValueWidget : Add context menu and "Custom" option This makes the Widget more useful in technical workflows where you might want string substitutions, expressions or wildcards. --- Changes.md | 3 ++ .../GafferImageUI/ChannelPlugValueWidget.py | 48 +++++++++++++++++-- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/Changes.md b/Changes.md index de70b08ef0b..8dfeda05c5f 100644 --- a/Changes.md +++ b/Changes.md @@ -22,6 +22,9 @@ Improvements - Source channels can optionally be deleted after shuffling. - Overwriting of destination channels can optionally be avoided. - Added `missingSourceMode` plug to determine behaviour when a source channel doesn't exist. +- NodeEditor : Improved image channel selectors : + - Added "Custom" option, to allow strings to be entered manually. + - Added right-click context menu. Fixes ----- diff --git a/python/GafferImageUI/ChannelPlugValueWidget.py b/python/GafferImageUI/ChannelPlugValueWidget.py index bae12d90030..e721f26fc29 100644 --- a/python/GafferImageUI/ChannelPlugValueWidget.py +++ b/python/GafferImageUI/ChannelPlugValueWidget.py @@ -62,13 +62,20 @@ class ChannelPlugValueWidget( GafferUI.PlugValueWidget ) : def __init__( self, plugs, **kw ) : - self.__menuButton = GafferUI.MenuButton( menu = GafferUI.Menu( Gaffer.WeakMethod( self.__menuDefinition ) ) ) + column = GafferUI.ListContainer( spacing = 4 ) + GafferUI.PlugValueWidget.__init__( self, column, plugs, **kw ) - GafferUI.PlugValueWidget.__init__( self, self.__menuButton, plugs, **kw ) + with column : + + self.__menuButton = GafferUI.MenuButton( menu = GafferUI.Menu( Gaffer.WeakMethod( self.__menuDefinition ) ) ) + self._addPopupMenu( self.__menuButton ) + self.__customValueWidget = GafferUI.StringPlugValueWidget( plugs ) self.__availableChannels = set() self.__currentChannel = None + self.__customValueWidget.setVisible( self.__isCustom() ) + def _auxiliaryPlugs( self, plug ) : name = Gaffer.Metadata.value( plug, "channelPlugValueWidget:imagePlugName" ) or "in" @@ -95,13 +102,24 @@ def _updateFromValues( self, values, exception ) : self.__availableChannels = set().union( *[ v["availableChannels"] for v in values ] ) self.__currentChannel = sole( v["value"] for v in values ) - label = self.__extraChannels().get( self.__currentChannel, self.__currentChannel ) + isCustom = self.__isCustom() + self.__customValueWidget.setVisible( isCustom ) + + if isCustom : + label = "Custom" + else : + label = self.__extraChannels().get( self.__currentChannel, self.__currentChannel ) + self.__menuButton.setText( label if label is not None else "---" ) self.__menuButton.setErrored( exception is not None ) + def _updateFromMetadata( self ) : + + self._requestUpdateFromValues() + def _updateFromEditable( self ) : self.__menuButton.setEnabled( self._editable() ) @@ -114,13 +132,15 @@ def __menuDefinition( self ) : if channel not in availableChannels : availableChannels.add( channel ) + isCustom = self.__isCustom() + result = IECore.MenuDefinition() for channel in GafferImage.ImageAlgo.sortedChannelNames( availableChannels ) : result.append( "/{}".format( channel.replace( ".", "/" ) ), { "command" : functools.partial( Gaffer.WeakMethod( self.__setValue ), value = channel ), - "checkBox" : channel == self.__currentChannel, + "checkBox" : not isCustom and channel == self.__currentChannel, } ) @@ -139,6 +159,15 @@ def __menuDefinition( self ) : if not result.items() : result.append( "/No Channels Available", { "active" : False } ) + result.append( "/CustomDivider", { "divider" : True } ) + result.append( + "/Custom", + { + "command" : Gaffer.WeakMethod( self.__applyCustom ), + "checkBox" : isCustom, + } + ) + return result def __setValue( self, unused, value ) : @@ -146,6 +175,17 @@ def __setValue( self, unused, value ) : with Gaffer.UndoScope( next( iter( self.getPlugs() ) ).ancestor( Gaffer.ScriptNode ) ) : for plug in self.getPlugs() : plug.setValue( value ) + Gaffer.Metadata.deregisterValue( plug, "channelPlugValueWidget:isCustom" ) + + def __applyCustom( self, unused ) : + + with Gaffer.UndoScope( next( iter( self.getPlugs() ) ).ancestor( Gaffer.ScriptNode ) ) : + for plug in self.getPlugs() : + Gaffer.Metadata.registerValue( plug, "channelPlugValueWidget:isCustom", True ) + + def __isCustom( self ) : + + return any( Gaffer.Metadata.value( p, "channelPlugValueWidget:isCustom" ) for p in self.getPlugs() ) def __extraChannels( self ) : From 7a8b5d54e4efca4ecc240193b6e7187c7aef80f5 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Wed, 13 Dec 2023 14:44:04 +0000 Subject: [PATCH 7/7] [Un]Premultiply, ImageScatter : Use ChannelPlugValueWidget I can't see a reason not to use it, now it's a bit more versatile. --- python/GafferImageUI/PremultiplyUI.py | 2 ++ python/GafferImageUI/UnpremultiplyUI.py | 2 ++ python/GafferSceneUI/ImageScatterUI.py | 5 ++++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/python/GafferImageUI/PremultiplyUI.py b/python/GafferImageUI/PremultiplyUI.py index 8581573790a..aa924b1d633 100644 --- a/python/GafferImageUI/PremultiplyUI.py +++ b/python/GafferImageUI/PremultiplyUI.py @@ -61,6 +61,8 @@ remain the same as the input. """, + "plugValueWidget:type", "GafferImageUI.ChannelPlugValueWidget", + ], } diff --git a/python/GafferImageUI/UnpremultiplyUI.py b/python/GafferImageUI/UnpremultiplyUI.py index 7321d1eaaa6..748a1a2de2e 100644 --- a/python/GafferImageUI/UnpremultiplyUI.py +++ b/python/GafferImageUI/UnpremultiplyUI.py @@ -63,6 +63,8 @@ remain the same as the input. """, + "plugValueWidget:type", "GafferImageUI.ChannelPlugValueWidget", + ], } diff --git a/python/GafferSceneUI/ImageScatterUI.py b/python/GafferSceneUI/ImageScatterUI.py index 1004b7a2b18..f063bb37a12 100644 --- a/python/GafferSceneUI/ImageScatterUI.py +++ b/python/GafferSceneUI/ImageScatterUI.py @@ -107,7 +107,10 @@ The image channel used to modulate the density of the scattered points. Black pixels will receive no points and white pixels will receive the full amount as defined by the `density` plug. - """ + """, + + "plugValueWidget:type", "GafferImageUI.ChannelPlugValueWidget", + "channelPlugValueWidget:imagePlugName", "image", ],