From 0675a9f1ccc51739db69f3c17bcf4925ae80a52b Mon Sep 17 00:00:00 2001 From: Daniel Dresser Date: Mon, 25 Nov 2024 18:21:20 -0800 Subject: [PATCH 1/7] TweakPlug : Expose modeToString --- include/Gaffer/TweakPlug.h | 4 +-- src/Gaffer/TweakPlug.cpp | 64 +++++++++++++++++++------------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/include/Gaffer/TweakPlug.h b/include/Gaffer/TweakPlug.h index ab67d21cb50..21354424063 100644 --- a/include/Gaffer/TweakPlug.h +++ b/include/Gaffer/TweakPlug.h @@ -123,6 +123,8 @@ class GAFFER_API TweakPlug : public Gaffer::ValuePlug MissingMode missingMode = MissingMode::Error ) const; + static const char *modeToString( Gaffer::TweakPlug::Mode mode ); + private : Gaffer::ValuePlug *valuePlugInternal(); @@ -146,8 +148,6 @@ class GAFFER_API TweakPlug : public Gaffer::ValuePlug void applyReplaceTweak( const IECore::Data *sourceData, IECore::Data *tweakData ) const; - static const char *modeToString( Gaffer::TweakPlug::Mode mode ); - }; IE_CORE_DECLAREPTR( TweakPlug ) diff --git a/src/Gaffer/TweakPlug.cpp b/src/Gaffer/TweakPlug.cpp index 7a1fb169f35..64053e91bbb 100644 --- a/src/Gaffer/TweakPlug.cpp +++ b/src/Gaffer/TweakPlug.cpp @@ -301,6 +301,38 @@ bool TweakPlug::applyTweak( IECore::CompoundData *parameters, MissingMode missin ); } +const char *TweakPlug::modeToString( Gaffer::TweakPlug::Mode mode ) +{ + switch( mode ) + { + case Gaffer::TweakPlug::Replace : + return "Replace"; + case Gaffer::TweakPlug::Add : + return "Add"; + case Gaffer::TweakPlug::Subtract : + return "Subtract"; + case Gaffer::TweakPlug::Multiply : + return "Multiply"; + case Gaffer::TweakPlug::Remove : + return "Remove"; + case Gaffer::TweakPlug::Create : + return "Create"; + case Gaffer::TweakPlug::Min : + return "Min"; + case Gaffer::TweakPlug::Max : + return "Max"; + case Gaffer::TweakPlug::ListAppend : + return "ListAppend"; + case Gaffer::TweakPlug::ListPrepend : + return "ListPrepend"; + case Gaffer::TweakPlug::ListRemove : + return "ListRemove"; + case Gaffer::TweakPlug::CreateIfMissing : + return "CreateIfMissing"; + } + return "Invalid"; +} + void TweakPlug::applyNumericTweak( const IECore::Data *sourceData, const IECore::Data *tweakData, @@ -436,38 +468,6 @@ void TweakPlug::applyReplaceTweak( const IECore::Data *sourceData, IECore::Data } } -const char *TweakPlug::modeToString( Gaffer::TweakPlug::Mode mode ) -{ - switch( mode ) - { - case Gaffer::TweakPlug::Replace : - return "Replace"; - case Gaffer::TweakPlug::Add : - return "Add"; - case Gaffer::TweakPlug::Subtract : - return "Subtract"; - case Gaffer::TweakPlug::Multiply : - return "Multiply"; - case Gaffer::TweakPlug::Remove : - return "Remove"; - case Gaffer::TweakPlug::Create : - return "Create"; - case Gaffer::TweakPlug::Min : - return "Min"; - case Gaffer::TweakPlug::Max : - return "Max"; - case Gaffer::TweakPlug::ListAppend : - return "ListAppend"; - case Gaffer::TweakPlug::ListPrepend : - return "ListPrepend"; - case Gaffer::TweakPlug::ListRemove : - return "ListRemove"; - case Gaffer::TweakPlug::CreateIfMissing : - return "CreateIfMissing"; - } - return "Invalid"; -} - ////////////////////////////////////////////////////////////////////////// // TweaksPlug ////////////////////////////////////////////////////////////////////////// From 7631733d08eb3acc074d9ae702eff15c75ab5b66 Mon Sep 17 00:00:00 2001 From: Daniel Dresser Date: Tue, 24 Dec 2024 14:55:13 -0800 Subject: [PATCH 2/7] TweakPlugValueWidget : Show list modes for Int64VectorData --- python/GafferUI/TweakPlugValueWidget.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/GafferUI/TweakPlugValueWidget.py b/python/GafferUI/TweakPlugValueWidget.py index e0d0221c69a..f4b58ebe85f 100644 --- a/python/GafferUI/TweakPlugValueWidget.py +++ b/python/GafferUI/TweakPlugValueWidget.py @@ -189,6 +189,7 @@ def __validModes( plug ) : if type( plug.parent()["value"] ) in [ Gaffer.BoolVectorDataPlug, Gaffer.IntVectorDataPlug, + Gaffer.Int64VectorDataPlug, Gaffer.FloatVectorDataPlug, Gaffer.StringVectorDataPlug, Gaffer.InternedStringVectorDataPlug, From 5e5d5d3744018a43dcd16969e24784d149f87adb Mon Sep 17 00:00:00 2001 From: Daniel Dresser Date: Mon, 23 Dec 2024 17:20:35 -0800 Subject: [PATCH 3/7] TweakPlugTest : Document weird edge case for ListAppend --- python/GafferTest/TweakPlugTest.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/python/GafferTest/TweakPlugTest.py b/python/GafferTest/TweakPlugTest.py index 772fdd5584c..897f37b7aca 100644 --- a/python/GafferTest/TweakPlugTest.py +++ b/python/GafferTest/TweakPlugTest.py @@ -345,6 +345,24 @@ def testTweakInternedString( self ) : self.assertTrue( result ) self.assertEqual( data["a"], IECore.InternedStringData( "stringValue" ) ) + def testListAppendWeirdCornerCase( self ) : + + tweak = Gaffer.TweakPlug( "a", IECore.V3fData( imath.V3f( 7 ) ), Gaffer.TweakPlug.Mode.ListAppend ) + + data = IECore.CompoundData() + + # A weird consequence of ListAppend being treated as a "Create" if there are no existing list entries: + # A listAppend will succeed when there is no existing data, even if the type of the tweak is completely + # invalid for this mode. + tweak.applyTweak( data ) + self.assertEqual( data["a"], IECore.V3fData( imath.V3f( 7 ) ) ) + + # Once there is existing data, we get the expected error about the type not being valid. + with self.assertRaisesRegex( Exception, + 'Cannot apply tweak with mode ListAppend to "a" : Data type V3fDataBase not supported.' + ) : + tweak.applyTweak( data ) + def testPathMatcherListOperations( self ) : data = IECore.CompoundData( From 696b150030912341a0677e6928f3b9418f12a98b Mon Sep 17 00:00:00 2001 From: Daniel Dresser Date: Tue, 24 Dec 2024 14:35:18 -0800 Subject: [PATCH 4/7] TweakPlugTest : Add test case for keeping geometric interpretation --- python/GafferTest/TweakPlugTest.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/python/GafferTest/TweakPlugTest.py b/python/GafferTest/TweakPlugTest.py index 897f37b7aca..e91885c1125 100644 --- a/python/GafferTest/TweakPlugTest.py +++ b/python/GafferTest/TweakPlugTest.py @@ -330,6 +330,20 @@ def testTweakModes( self ) : self.assertFalse( tweaks.applyTweaks( parameters ) ) self.assertNotIn( "f", parameters ) + def testPreservesInterpretation( self ) : + + # Check that the interpretation comes from the source data, not the tweak. + data = IECore.CompoundData( + { + "a" : IECore.V3fData( imath.V3f( 1, 2, 3 ), IECore.GeometricData.Interpretation.Normal ), + } + ) + + tweak = Gaffer.TweakPlug( "a", IECore.V3fData( imath.V3f( 0.5 ) ), Gaffer.TweakPlug.Mode.Add ) + + tweak.applyTweak( data ) + self.assertEqual( data["a"], IECore.V3fData( imath.V3f( 1.5, 2.5, 3.5 ), IECore.GeometricData.Interpretation.Normal ) ) + def testTweakInternedString( self ) : data = IECore.CompoundData( From a46c07c7b23c3e5cf3a6a2d18a025cd76934ae63 Mon Sep 17 00:00:00 2001 From: Daniel Dresser Date: Tue, 24 Dec 2024 17:06:56 -0800 Subject: [PATCH 5/7] TweakPlugTest : Test exceptions for invalid types for numeric ops --- python/GafferTest/TweakPlugTest.py | 36 ++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/python/GafferTest/TweakPlugTest.py b/python/GafferTest/TweakPlugTest.py index e91885c1125..850c3ed4247 100644 --- a/python/GafferTest/TweakPlugTest.py +++ b/python/GafferTest/TweakPlugTest.py @@ -377,6 +377,42 @@ def testListAppendWeirdCornerCase( self ) : ) : tweak.applyTweak( data ) + def testInvalidNumeric( self ) : + + parameters = IECore.CompoundData( + { + "a" : "foo", + "b" : IECore.IntVectorData( [ 0, 1, 2 ] ), + "c" : IECore.StringVectorData( [ "gouda", "cheddar", "cheddar", "swiss" ] ), + } + ) + + tweaks = Gaffer.TweaksPlug() + tweaks.addChild( Gaffer.TweakPlug( "a", "foo", Gaffer.TweakPlug.Mode.Add ) ) + + with self.assertRaisesRegex( Exception, + 'Cannot apply tweak with mode Add to "a" : Data type StringData not supported.' + ) : + tweaks.applyTweaks( parameters ) + + del tweaks[0] + + tweaks.addChild( Gaffer.TweakPlug( "b", IECore.IntVectorData( [ 0, 1, 2 ] ), Gaffer.TweakPlug.Mode.Multiply ) ) + + with self.assertRaisesRegex( Exception, + 'Cannot apply tweak with mode Multiply to "b" : Data type IntVectorData not supported.' + ) : + tweaks.applyTweaks( parameters ) + + del tweaks[0] + + tweaks.addChild( Gaffer.TweakPlug( "c", IECore.StringVectorData( [ "foo" ] ), Gaffer.TweakPlug.Mode.Subtract ) ) + + with self.assertRaisesRegex( Exception, + 'Cannot apply tweak with mode Subtract to "c" : Data type StringVectorData not supported.' + ) : + tweaks.applyTweaks( parameters ) + def testPathMatcherListOperations( self ) : data = IECore.CompoundData( From fc80eab06a340b31de7e3694fc97f114749cfce8 Mon Sep 17 00:00:00 2001 From: Daniel Dresser Date: Thu, 21 Nov 2024 17:28:29 -0800 Subject: [PATCH 6/7] TweakPlug : Added applyElementwiseTweak() and fixed geom interpret --- Changes.md | 6 + include/Gaffer/TweakPlug.h | 58 +++- include/Gaffer/TweakPlug.inl | 142 +++++++-- src/Gaffer/TweakPlug.cpp | 599 ++++++++++++++++++++++++++--------- 4 files changed, 610 insertions(+), 195 deletions(-) diff --git a/Changes.md b/Changes.md index 04ab9c90484..d3015410bf4 100644 --- a/Changes.md +++ b/Changes.md @@ -12,6 +12,12 @@ Fixes - VisualiserTool : - Fixed bug where the value dragged from the visualiser would be slightly different from the initial value on button press. (#6191) - Fixed error when trying to visualise data unsupported data. +- TweakPlug : Fixed preservation of geometric interpretation when tweaking V3f values. + +API +--- + +- TweakPlug : Added `applyElementwiseTweak()` method, for tweaking elements of a `*VectorData`. 1.5.2.0 (relative to 1.5.1.0) ======= diff --git a/include/Gaffer/TweakPlug.h b/include/Gaffer/TweakPlug.h index 21354424063..814b991ecd4 100644 --- a/include/Gaffer/TweakPlug.h +++ b/include/Gaffer/TweakPlug.h @@ -42,6 +42,11 @@ #include "Gaffer/StringPlug.h" #include "Gaffer/TypedPlug.h" +#include "IECore/VectorTypedData.h" + +#include "boost/dynamic_bitset.hpp" + + namespace Gaffer { @@ -123,6 +128,40 @@ class GAFFER_API TweakPlug : public Gaffer::ValuePlug MissingMode missingMode = MissingMode::Error ) const; + + struct DataAndIndices{ + IECore::DataPtr data; + IECore::IntVectorDataPtr indices; + }; + + + + /// As above, but applying the tweak to individual elements of VectorTypedData, + /// as specified by `mask`. + template + bool applyElementwiseTweak( + /// Signature : const DataAndIndices functor( const std::string &valueName, const bool withFallback ). + /// Passing `withFallback=False` specifies that no fallback value should be returned in place of missing data. + /// \returns DataAndIndices with members set to `nullptr` if `valueName` is invalid. + GetDataFunctor &&getDataFunctor, + + /// Signature : bool functor( const std::string &valueName, DataAndIndices &newData ). + /// Passing `nullptr` in `newData.data` removes the entry for `valueName`. + /// If the getDataFunctor ever returns indices set to non-null, then setDataFunctor needs + /// to deal with receiving modified indices. + /// \returns true if the value was set or erased, false if erasure failed. + SetDataFunctor &&setDataFunctor, + + // Size of array to make for `Create` mode. + size_t createSize, + + // If specified, this bitset must have the same size as the data. Only elements of the data + // corresponding to where the mask is true will be tweaked. + const boost::dynamic_bitset<> *mask = nullptr, + + MissingMode missingMode = MissingMode::Error + ) const; + static const char *modeToString( Gaffer::TweakPlug::Mode mode ); private : @@ -130,23 +169,12 @@ class GAFFER_API TweakPlug : public Gaffer::ValuePlug Gaffer::ValuePlug *valuePlugInternal(); const Gaffer::ValuePlug *valuePlugInternal() const; - void applyNumericTweak( - const IECore::Data *sourceData, - const IECore::Data *tweakData, - IECore::Data *destData, - TweakPlug::Mode mode, - const std::string &tweakName - ) const; + static void applyTweakInternal( IECore::Data *data, const IECore::Data *tweakData, TweakPlug::Mode mode, const std::string &name ); - void applyListTweak( - const IECore::Data *sourceData, - const IECore::Data *tweakData, - IECore::Data *destData, - TweakPlug::Mode mode, - const std::string &tweakName - ) const; + static IECore::DataPtr createVectorDataFromElement( const IECore::Data *elementData, size_t size, bool useElementValueAsDefault, const std::string &name ); + + static void applyVectorElementTweak( IECore::Data *vectorData, const IECore::Data *tweakData, IECore::IntVectorData *indicesData, TweakPlug::Mode mode, const std::string &name, const boost::dynamic_bitset<> *mask ); - void applyReplaceTweak( const IECore::Data *sourceData, IECore::Data *tweakData ) const; }; diff --git a/include/Gaffer/TweakPlug.inl b/include/Gaffer/TweakPlug.inl index 57a195f6036..6c11f9eca21 100644 --- a/include/Gaffer/TweakPlug.inl +++ b/include/Gaffer/TweakPlug.inl @@ -77,11 +77,11 @@ bool TweakPlug::applyTweak( if( mode == Gaffer::TweakPlug::Remove ) { - return setDataFunctor( name, nullptr ); + return setDataFunctor( name, nullptr ); } - IECore::DataPtr newData = Gaffer::PlugAlgo::getValueAsData( valuePlug() ); - if( !newData ) + IECore::DataPtr tweakData = Gaffer::PlugAlgo::getValueAsData( valuePlug() ); + if( !tweakData ) { throw IECore::Exception( fmt::format( "Cannot apply tweak to \"{}\" : Value plug has unsupported type \"{}\"", name, valuePlug()->typeName() ) @@ -90,26 +90,19 @@ bool TweakPlug::applyTweak( if( mode == Gaffer::TweakPlug::Create ) { - return setDataFunctor( name, newData ); + return setDataFunctor( name, tweakData ); } const IECore::Data *currentValue = getDataFunctor( name, /* withFallback = */ mode != Gaffer::TweakPlug::CreateIfMissing ); if( IECore::runTimeCast( currentValue ) ) { - if( const IECore::StringData *s = IECore::runTimeCast( newData.get() ) ) + if( const IECore::StringData *s = IECore::runTimeCast( tweakData.get() ) ) { - newData = new IECore::InternedStringData( s->readable() ); + tweakData = new IECore::InternedStringData( s->readable() ); } } - if( currentValue && currentValue->typeId() != newData->typeId() ) - { - throw IECore::Exception( - fmt::format( "Cannot apply tweak to \"{}\" : Value of type \"{}\" does not match parameter of type \"{}\"", name, currentValue->typeName(), newData->typeName() ) - ); - } - if( !currentValue ) { if( @@ -118,7 +111,7 @@ bool TweakPlug::applyTweak( mode == Gaffer::TweakPlug::CreateIfMissing ) { - setDataFunctor( name, newData ); + setDataFunctor( name, tweakData ); return true; } else if( missingMode == Gaffer::TweakPlug::MissingMode::Ignore || mode == Gaffer::TweakPlug::ListRemove ) @@ -128,37 +121,126 @@ bool TweakPlug::applyTweak( throw IECore::Exception( fmt::format( "Cannot apply tweak with mode {} to \"{}\" : This parameter does not exist", modeToString( mode ), name ) ); } + if( mode == Gaffer::TweakPlug::CreateIfMissing ) + { + // \todo - It would make more sense if this returned false ( this tweak technically is applying, but it's + // not doing anything ). Fixing this now would technically be a compatibility break though. If we fixed + //this, we could clarify the documentation of applyTweak: + // instead of "returns true if any tweaks were applied" it could be "returns true if any changes were made". + return true; + } + + // valueTweakData + IECore::DataPtr resultData = currentValue->copy(); + + applyTweakInternal( resultData.get(), tweakData.get(), mode, name ); + setDataFunctor( name, resultData ); + + + return true; +} + +template +bool TweakPlug::applyElementwiseTweak( + GetDataFunctor &&getDataFunctor, + SetDataFunctor &&setDataFunctor, + size_t createSize, + const boost::dynamic_bitset<> *mask, + MissingMode missingMode +) const +{ + if( !enabledPlug()->getValue() ) + { + return false; + } + + const std::string name = namePlug()->getValue(); + if( name.empty() ) + { + return false; + } + + const Mode mode = static_cast( modePlug()->getValue() ); + + if( mode == Gaffer::TweakPlug::Remove ) + { + return setDataFunctor( name, DataAndIndices() ); + } + + IECore::DataPtr tweakData = Gaffer::PlugAlgo::getValueAsData( valuePlug() ); + if( !tweakData ) + { + throw IECore::Exception( + fmt::format( "Cannot apply tweak to \"{}\" : Value plug has unsupported type \"{}\"", name, valuePlug()->typeName() ) + ); + } + + DataAndIndices current; + if( mode != Gaffer::TweakPlug::Create ) + { + current = getDataFunctor( name, /* withFallback = */ mode != Gaffer::TweakPlug::CreateIfMissing ); + } + if( - mode == Gaffer::TweakPlug::Add || - mode == Gaffer::TweakPlug::Subtract || - mode == Gaffer::TweakPlug::Multiply || - mode == Gaffer::TweakPlug::Min || - mode == Gaffer::TweakPlug::Max + mode == Gaffer::TweakPlug::Create || + ( !current.data && ( + mode == Gaffer::TweakPlug::CreateIfMissing || + mode == Gaffer::TweakPlug::ListAppend || + mode == Gaffer::TweakPlug::ListPrepend + ) ) ) { - applyNumericTweak( currentValue, newData.get(), newData.get(), mode, name ); + DataAndIndices result; + if( mask ) + { + result.data = createVectorDataFromElement( tweakData.get(), createSize, false, name ); + + applyVectorElementTweak( result.data.get(), tweakData.get(), nullptr, TweakPlug::Replace, name, mask ); + } + else + { + result.data = createVectorDataFromElement( tweakData.get(), createSize, true, name ); + } + return setDataFunctor( name, result ); } - else if( - mode == TweakPlug::ListAppend || - mode == TweakPlug::ListPrepend || - mode == TweakPlug::ListRemove - ) + + if( IECore::runTimeCast( current.data ) ) + { + if( const IECore::StringData *s = IECore::runTimeCast( tweakData.get() ) ) + { + tweakData = new IECore::InternedStringData( s->readable() ); + } + } + + if( !current.data ) { - applyListTweak( currentValue, newData.get(), newData.get(), mode, name ); + if( missingMode == Gaffer::TweakPlug::MissingMode::Ignore || mode == Gaffer::TweakPlug::ListRemove ) + { + return false; + } + throw IECore::Exception( fmt::format( "Cannot apply tweak with mode {} to \"{}\" : This parameter does not exist", modeToString( mode ), name ) ); } - else if( mode == TweakPlug::Replace ) + + if( mode == Gaffer::TweakPlug::CreateIfMissing ) { - applyReplaceTweak( currentValue, newData.get() ); + // \todo - See \todo in applyTweak about this return value. + return true; } - if( mode != Gaffer::TweakPlug::CreateIfMissing ) + DataAndIndices result; + result.data = current.data->copy(); + if( current.indices ) { - setDataFunctor( name, newData ); + result.indices = current.indices->copy(); } + applyVectorElementTweak( result.data.get(), tweakData.get(), result.indices.get(), mode, name, mask ); + setDataFunctor( name, result ); return true; } + + template bool TweaksPlug::applyTweaks( GetDataFunctor &&getDataFunctor, diff --git a/src/Gaffer/TweakPlug.cpp b/src/Gaffer/TweakPlug.cpp index 64053e91bbb..c5ba8b98d7e 100644 --- a/src/Gaffer/TweakPlug.cpp +++ b/src/Gaffer/TweakPlug.cpp @@ -45,11 +45,11 @@ #include "IECore/StringAlgo.h" #include "IECore/TypeTraits.h" +#include "fmt/format.h" + #include "boost/algorithm/string/join.hpp" #include "boost/algorithm/string/replace.hpp" -#include "fmt/format.h" - #include using namespace std; @@ -59,22 +59,13 @@ using namespace Gaffer; ////////////////////////////////////////////////////////////////////////// // Internal utilities ////////////////////////////////////////////////////////////////////////// - namespace { -/// \todo - if these make sense, I guess they should be pushed back to cortex - -// IsColorTypedData -template< typename T > struct IsColorTypedData : boost::mpl::and_< TypeTraits::IsTypedData, TypeTraits::IsColor< typename TypeTraits::ValueType::type > > {}; - -// SupportsArithmeticData -template< typename T > struct SupportsArithData : boost::mpl::or_< TypeTraits::IsNumericSimpleTypedData, TypeTraits::IsVecTypedData, IsColorTypedData> {}; - template T vectorAwareMin( const T &v1, const T &v2 ) { - if constexpr( TypeTraits::IsVec::value || TypeTraits::IsColor::value ) + if constexpr( IECore::TypeTraits::IsVec::value || IECore::TypeTraits::IsColor::value ) { T result; for( size_t i = 0; i < T::dimensions(); ++i ) @@ -92,7 +83,7 @@ T vectorAwareMin( const T &v1, const T &v2 ) template T vectorAwareMax( const T &v1, const T &v2 ) { - if constexpr( TypeTraits::IsVec::value || TypeTraits::IsColor::value ) + if constexpr( IECore::TypeTraits::IsVec::value || IECore::TypeTraits::IsColor::value ) { T result; for( size_t i = 0; i < T::dimensions(); ++i ) @@ -107,10 +98,70 @@ T vectorAwareMax( const T &v1, const T &v2 ) } } +template< typename T > +T applyNumericTweak( + const T &source, + const T &tweak, + TweakPlug::Mode mode, + const std::string &tweakName +) +{ + if constexpr( + ( std::is_arithmetic_v && !std::is_same_v< T, bool > ) || + IECore::TypeTraits::IsVec::value || + IECore::TypeTraits::IsColor::value + ) + { + switch( mode ) + { + case TweakPlug::Add : + return source + tweak; + case TweakPlug::Subtract : + return source - tweak; + case TweakPlug::Multiply : + return source * tweak; + case TweakPlug::Min : + return vectorAwareMin( source, tweak ); + case TweakPlug::Max : + return vectorAwareMax( source, tweak ); + case TweakPlug::ListAppend : + case TweakPlug::ListPrepend : + case TweakPlug::ListRemove : + case TweakPlug::Replace : + case TweakPlug::Remove : + case TweakPlug::Create : + case TweakPlug::CreateIfMissing : + throw IECore::Exception( + fmt::format( + "Cannot apply tweak with mode {} using applyNumericTweak.", + TweakPlug::modeToString( mode ) + ) + ); + default: + throw IECore::Exception( fmt::format( "Not a valid tweak mode: {}.", mode ) ); + } + } + else + { + // NOTE: If we are operating on variables that aren't actually stored in a Data, then the + // data type reported here may not be technically correct - for example, we might want to + // call this on elements of a StringVectorData, in which case this would report a type of + // "StringData", but there is nothing of actual type "StringData". This message still + // communicates the actual problem though ( we don't support arithmetic on strings ). + + throw IECore::Exception( + fmt::format( + "Cannot apply tweak with mode {} to \"{}\" : Data type {} not supported.", + TweakPlug::modeToString( mode ), tweakName, IECore::TypedData::staticTypeName() + ) + ); + } +} + template -vector tweakedList( const std::vector &source, const std::vector &tweak, TweakPlug::Mode mode ) +std::vector tweakedList( const std::vector &source, const std::vector &tweak, TweakPlug::Mode mode ) { - vector result = source; + std::vector result = source; struct HashFunc { @@ -148,6 +199,175 @@ vector tweakedList( const std::vector &source, const std::vector &tweak return result; } +template +T applyListTweak( + const T &source, + const T &tweak, + TweakPlug::Mode mode, + const std::string &tweakName +) +{ + // \todo - would look cleaner if we had an IsVector in TypeTraits rather than needed to wrap + // this is a Data just to check if it's an std::vector + if constexpr( IECore::TypeTraits::IsVectorTypedData< IECore::TypedData >::value ) + { + return tweakedList( source, tweak, mode ); + } + else if constexpr( std::is_same_v ) + { + IECore::PathMatcher result = source; + if( mode == TweakPlug::ListRemove ) + { + result.removePaths( tweak ); + } + else + { + result.addPaths( tweak ); + } + return result; + } + else if constexpr( std::is_same_v ) + { + std::vector sourceVector; + IECore::StringAlgo::tokenize( source, ' ', sourceVector ); + std::vector tweakVector; + IECore::StringAlgo::tokenize( tweak, ' ', tweakVector ); + return boost::algorithm::join( tweakedList( sourceVector, tweakVector, mode ), " " ); + } + else + { + throw IECore::Exception( + fmt::format( + "Cannot apply tweak with mode {} to \"{}\" : Data type {} not supported.", + TweakPlug::modeToString( mode ), tweakName, IECore::TypedData::staticTypeName() + ) + ); + } +} + +template< typename T > +T applyReplaceTweak( + const T &source, + const T &tweak +) +{ + if constexpr( std::is_same_v< T, std::string > ) + { + return boost::replace_all_copy( tweak, "{source}", source ); + } + else if constexpr ( std::is_same_v< T, IECore::InternedString > ) + { + return IECore::InternedString( boost::replace_all_copy( tweak.string(), "{source}", source.string() ) ); + } + else + { + return tweak; + } +} + +template< typename T > +T applyValueTweak( + const T &source, + const T &tweak, + TweakPlug::Mode mode, + const std::string &tweakName +) +{ + if( + mode == Gaffer::TweakPlug::Add || + mode == Gaffer::TweakPlug::Subtract || + mode == Gaffer::TweakPlug::Multiply || + mode == Gaffer::TweakPlug::Min || + mode == Gaffer::TweakPlug::Max + ) + { + return applyNumericTweak( source, tweak, mode, tweakName ); + } + else if( + mode == TweakPlug::ListAppend || + mode == TweakPlug::ListPrepend || + mode == TweakPlug::ListRemove + ) + { + return applyListTweak( source, tweak, mode, tweakName ); + } + else if( mode == TweakPlug::Replace ) + { + return applyReplaceTweak( source, tweak ); + } + else + { + throw IECore::Exception( + fmt::format( + "Cannot apply tweak with mode {} using applyValueTweak.", + TweakPlug::modeToString( mode ) + ) + ); + } +} + + +template +void removeUnusedElements( std::vector &indices, std::vector &data ) +{ + std::vector used( data.size(), -1 ); + + for( const int &i : indices ) + { + used[i] = 1; + } + + int accum = 0; + for( int &i : used ) + { + if( i != -1 ) + { + i = accum; + accum += 1; + } + } + + if( accum == (int)data.size() ) + { + // All elements were used + return; + } + + std::vector result; + result.reserve( accum ); + for( size_t j = 0; j < data.size(); j++ ) + { + if( used[j] != -1 ) + { + result.push_back( data[j] ); + } + } + + for( int &i : indices ) + { + i = used[i]; + } + + data.swap( result ); +} + +template< typename T> +bool constexpr hasZeroConstructor() +{ + // Some types, like V3f and Color3f, won't default initialize unless we explicitly + // pass 0 to the constructor. Other types don't have a constructor that accepts 0, + // so we need to distinguish the two somehow. Currently, I'm using a blacklist of + // types that don't need to be initialized to zero ... my rationale is that if a new + // type is added, I would rather get a compile error than get uninitialized memory. + return !( + IECore::TypeTraits::IsBox< T >::value || + IECore::TypeTraits::IsMatrix< T >::value || + IECore::TypeTraits::IsQuat< T >::value || + std::is_same_v< T, IECore::InternedString > || + std::is_same_v< T, std::string > + ); +} + } // namespace ////////////////////////////////////////////////////////////////////////// @@ -333,141 +553,6 @@ const char *TweakPlug::modeToString( Gaffer::TweakPlug::Mode mode ) return "Invalid"; } -void TweakPlug::applyNumericTweak( - const IECore::Data *sourceData, - const IECore::Data *tweakData, - IECore::Data *destData, - TweakPlug::Mode mode, - const std::string &tweakName -) const -{ - dispatch( - - destData, - - [&] ( auto data ) { - - using DataType = typename std::remove_pointer::type; - - if constexpr( SupportsArithData::value ) { - - const DataType *sourceDataCast = runTimeCast( sourceData ); - const DataType *tweakDataCast = runTimeCast( tweakData ); - - switch( mode ) - { - case TweakPlug::Add : - data->writable() = sourceDataCast->readable() + tweakDataCast->readable(); - break; - case TweakPlug::Subtract : - data->writable() = sourceDataCast->readable() - tweakDataCast->readable(); - break; - case TweakPlug::Multiply : - data->writable() = sourceDataCast->readable() * tweakDataCast->readable(); - break; - case TweakPlug::Min : - data->writable() = vectorAwareMin( sourceDataCast->readable(), tweakDataCast->readable() ); - break; - case TweakPlug::Max : - data->writable() = vectorAwareMax( sourceDataCast->readable(), tweakDataCast->readable() ); - break; - case TweakPlug::ListAppend : - case TweakPlug::ListPrepend : - case TweakPlug::ListRemove : - case TweakPlug::Replace : - case TweakPlug::Remove : - case TweakPlug::Create : - case TweakPlug::CreateIfMissing : - // These cases are unused - we handle them outside of numericTweak. - // But the compiler gets unhappy if we don't handle some cases. - assert( false ); - break; - } - } - else - { - throw IECore::Exception( - fmt::format( - "Cannot apply tweak with mode {} to \"{}\" : Data type {} not supported.", - modeToString( mode ), tweakName, sourceData->typeName() - ) - ); - } - } - ); -} - -void TweakPlug::applyListTweak( - const IECore::Data *sourceData, - const IECore::Data *tweakData, - IECore::Data *destData, - TweakPlug::Mode mode, - const std::string &tweakName -) const -{ - - // Despite being separate function arguments, `tweakData` and `destData` - // point to the _same object_, so we must be careful not to assign to - // `destData` until after we're done reading from `tweakData`. - /// \todo Use a single in-out function argument so that this is obvious. - - dispatch( - - destData, - - [&] ( auto data ) { - - using DataType = typename std::remove_pointer::type; - - if constexpr( TypeTraits::IsVectorTypedData::value ) - { - data->writable() = tweakedList( - static_cast( sourceData )->readable(), - static_cast( tweakData )->readable(), - mode - ); - } - else if constexpr( std::is_same_v ) - { - const PathMatcher newPaths = runTimeCast( tweakData )->readable(); - data->writable() = runTimeCast( sourceData )->readable(); - if( mode == TweakPlug::ListRemove ) - { - data->writable().removePaths( newPaths ); - } - else - { - data->writable().addPaths( newPaths ); - } - } - else if constexpr( std::is_same_v ) - { - vector sourceVector; - IECore::StringAlgo::tokenize( static_cast( sourceData )->readable(), ' ', sourceVector ); - vector tweakVector; - IECore::StringAlgo::tokenize( static_cast( tweakData )->readable(), ' ', tweakVector ); - data->writable() = boost::algorithm::join( tweakedList( sourceVector, tweakVector, mode ), " " ); - } - } - - ); -} - -void TweakPlug::applyReplaceTweak( const IECore::Data *sourceData, IECore::Data *tweakData ) const -{ - if( auto stringData = IECore::runTimeCast( tweakData ) ) - { - boost::replace_all( stringData->writable(), "{source}", static_cast( sourceData )->readable() ); - } - else if( auto internedStringData = IECore::runTimeCast( tweakData ) ) - { - internedStringData->writable() = boost::replace_all_copy( - internedStringData->readable().string(), - "{source}", static_cast( sourceData )->readable().string() - ); - } -} - ////////////////////////////////////////////////////////////////////////// // TweaksPlug ////////////////////////////////////////////////////////////////////////// @@ -540,3 +625,217 @@ bool TweaksPlug::applyTweaks( IECore::CompoundData *parameters, TweakPlug::Missi } return applied; } + + +void TweakPlug::applyTweakInternal( IECore::Data *data, const IECore::Data *tweakData, TweakPlug::Mode mode, const std::string &name ) +{ + IECore::dispatch( + data, + [&tweakData, &mode, &name] ( auto dataTyped ) + { + using DataType = typename std::remove_const_t >; + if constexpr( IECore::TypeTraits::IsTypedData< DataType >::value ) + { + auto tweakDataTyped = IECore::runTimeCast< const DataType >( tweakData ); + if( !tweakDataTyped ) + { + throw IECore::Exception( + fmt::format( "Cannot apply tweak to \"{}\" : Value of type \"{}\" does not match parameter of type \"{}\"", name, dataTyped->typeName(), tweakData->typeName() ) + ); + } + + auto &value = dataTyped->writable(); + value = applyValueTweak( value, tweakDataTyped->readable(), mode, name ); + } + else + { + throw IECore::Exception( fmt::format( "Cannot apply tweak to \"{}\" of type \"{}\"", name, dataTyped->typeName() ) ); + } + } + ); +} + +IECore::DataPtr TweakPlug::createVectorDataFromElement( const IECore::Data *elementData, size_t size, bool useElementValueAsDefault, const std::string &name ) +{ + return IECore::dispatch( + elementData, + [&size, &useElementValueAsDefault, &name] ( auto elementDataTyped ) -> IECore::DataPtr + { + using DataType = typename std::remove_const_t >; + using ValueType = typename DataType::ValueType; + + if constexpr( + IECore::TypeTraits::IsTypedData< DataType >::value && + !IECore::TypeTraits::IsVectorTypedData< DataType >::value && + + // A bunch of things we're not allowed to make vectors of + !IECore::TypeTraits::IsTransformationMatrix< ValueType >::value && + !IECore::TypeTraits::IsSpline< ValueType >::value && + !std::is_same_v< ValueType, IECore::PathMatcher > && + !std::is_same_v< ValueType, boost::posix_time::ptime > + ) + { + constexpr bool isGeometric = IECore::TypeTraits::IsGeometricTypedData< DataType >::value; + using VectorDataType = std::conditional_t< + isGeometric, + IECore::GeometricTypedData< std::vector< ValueType > >, + IECore::TypedData< std::vector< ValueType > > + >; + + typename VectorDataType::Ptr vectorData = new VectorDataType(); + + if( useElementValueAsDefault ) + { + vectorData->writable().resize( size, elementDataTyped->readable() ); + } + else + { + // Some types, like V3f and Color3f, won't default initialize unless we explicitly + // pass 0 to the constructor. Other types don't have a constructor that accepts 0, + // so we need to distinguish the two somehow. Currently, I'm using a blacklist of + // types that don't need to be initialized to zero ... my rationale is that if a new + // type is added, I would rather get a compile error than get uninitialized memory. + if constexpr( !hasZeroConstructor< ValueType >() ) + { + vectorData->writable().resize( size, ValueType() ); + } + else + { + vectorData->writable().resize( size, ValueType( 0 ) ); + } + } + + if constexpr( isGeometric ) + { + vectorData->setInterpretation( elementDataTyped->getInterpretation() ); + } + + return vectorData; + } + else + { + throw IECore::Exception( fmt::format( + "Invalid type \"{}\" for non-constant element-wise tweak \"{}\".", + elementDataTyped->typeName(), name + ) ); + } + } + ); +} + +void TweakPlug::applyVectorElementTweak( IECore::Data *vectorData, const IECore::Data *tweakData, IECore::IntVectorData *indicesData, TweakPlug::Mode mode, const std::string &name, const boost::dynamic_bitset<> *mask ) +{ + IECore::dispatch( vectorData, + [&tweakData, &indicesData, &mode, &name, &mask]( auto *vectorDataTyped ) + { + using SourceType = typename std::remove_pointer_t; + if constexpr( IECore::TypeTraits::IsVectorTypedData< SourceType >::value ) + { + auto &result = vectorDataTyped->writable(); + using ElementType = typename SourceType::ValueType::value_type; + using ElementDataType = IECore::TypedData< ElementType >; + + const ElementDataType* tweakDataTyped = IECore::runTimeCast< const ElementDataType >( tweakData ); + if( !tweakDataTyped ) + { + throw IECore::Exception( + fmt::format( + "Cannot apply tweak to \"{}\" : Parameter should be of type \"{}\" in order to apply " + "to an element of \"{}\", but got \"{}\" instead.", + name, ElementDataType::staticTypeName(), vectorDataTyped->typeName(), tweakData->typeName() + ) + ); + } + + auto &tweak = tweakDataTyped->readable(); + + if( mask && indicesData ) + { + // OK, this is a somewhat complex special case - we are only tweaking some data, based + // on indices, but some indices currently refer to the same data. If we end up tweaking + // only some of the indices that currently refer to the same data, then we're splitting + // it into two different values, and need to add a new piece of data to hold the new + // value. + + result.reserve( result.size() + mask->count() ); + + + std::vector &indices = indicesData->writable(); + std::unordered_map< int, int > tweakedIndices; + + if( mask->size() != indices.size() ) + { + throw IECore::Exception( + fmt::format( + "Invalid call to TweakPlug::applyElementwiseTweak. Mask size {} doesn't match indices size {}.", + mask->size(), indices.size() + ) + ); + } + + for( size_t i = 0 ; i < mask->size(); i++ ) + { + if( mask->test(i) ) + { + auto[ it, inserted ] = tweakedIndices.try_emplace( indices[i], result.size() ); + if( inserted ) + { + result.push_back( applyValueTweak( result[indices[i]], tweak, mode, name ) ); + } + indices[i] = it->second; + } + } + + + // If we actually ended up tweaking all indices that used a piece of data, that data is now + // abandoned, so we should now do a scan to remove unused data. + removeUnusedElements( indices, result ); + + result.shrink_to_fit(); + } + else if( mask ) + { + if( mask->size() != result.size() ) + { + throw IECore::Exception( + fmt::format( + "Invalid call to TweakPlug::applyElementwiseTweak. Mask size {} doesn't match data size {}.", + mask->size(), result.size() + ) + ); + } + + // If there are no indices, then we just modify the data where the mask is true + for( size_t i = 0 ; i < result.size(); i++ ) + { + if( mask->test(i) ) + { + result[i] = applyValueTweak( result[i], tweak, mode, name ); + } + } + } + else + { + // If there is no mask given, we're just modifying all the data, and it doesn't matter + // whether or not there are indices. + + // I probably should have paid more attention to what r-value references are in general, + // but in this case it seems like a pretty safe way to force this to work with the + // vector-of-bool weirdness + for( auto &&i : result ) + { + i = applyValueTweak( i, tweak, mode, name ); + } + } + } + else + { + throw IECore::Exception( fmt::format( + "Could not apply tweak to \"{}\" : Expected vector typed data, got \"{}\".", + name, vectorDataTyped->typeName() + ) ); + } + } + ); + +} From f610ba967bb5cd7161372b8aa6c12d0cc4e5de4f Mon Sep 17 00:00:00 2001 From: Daniel Dresser Date: Thu, 21 Nov 2024 16:49:14 -0800 Subject: [PATCH 7/7] Added PrimitiveVariableTweaks --- Changes.md | 5 + include/GafferScene/PrimitiveVariableTweaks.h | 104 +++ include/GafferScene/TypeIds.h | 2 +- .../PrimitiveVariableTweaksTest.py | 864 ++++++++++++++++++ .../PrimitiveVariableTweaksUI.py | 393 ++++++++ python/GafferSceneUI/__init__.py | 1 + src/GafferScene/PrimitiveVariableTweaks.cpp | 575 ++++++++++++ .../PrimitiveVariablesBinding.cpp | 12 + startup/gui/menus.py | 1 + 9 files changed, 1956 insertions(+), 1 deletion(-) create mode 100644 include/GafferScene/PrimitiveVariableTweaks.h create mode 100644 python/GafferSceneTest/PrimitiveVariableTweaksTest.py create mode 100644 python/GafferSceneUI/PrimitiveVariableTweaksUI.py create mode 100644 src/GafferScene/PrimitiveVariableTweaks.cpp diff --git a/Changes.md b/Changes.md index d3015410bf4..9f31a75ef8f 100644 --- a/Changes.md +++ b/Changes.md @@ -1,6 +1,11 @@ 1.5.x.x (relative to 1.5.2.0) ======= +Features +-------- + +- PrimitiveVariableTweaks : Added node for tweaking primitive variables. Can affect just part of a primitive based on ids or a mask. + Improvements ------------ diff --git a/include/GafferScene/PrimitiveVariableTweaks.h b/include/GafferScene/PrimitiveVariableTweaks.h new file mode 100644 index 00000000000..59aefb71a7f --- /dev/null +++ b/include/GafferScene/PrimitiveVariableTweaks.h @@ -0,0 +1,104 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2024, Image Engine Design Inc. 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. +// +////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "GafferScene/Deformer.h" + +#include "Gaffer/TweakPlug.h" + +namespace GafferScene +{ + +class GAFFERSCENE_API PrimitiveVariableTweaks : public Deformer +{ + + public : + + enum class SelectionMode + { + All, + IdList, + IdListPrimitiveVariable, + MaskPrimitiveVariable + }; + + explicit PrimitiveVariableTweaks( const std::string &name=defaultName() ); + ~PrimitiveVariableTweaks() override; + + GAFFER_NODE_DECLARE_TYPE( GafferScene::PrimitiveVariableTweaks, PrimitiveVariableTweaksTypeId, Deformer ); + + Gaffer::IntPlug *interpolationPlug(); + const Gaffer::IntPlug *interpolationPlug() const; + + Gaffer::IntPlug *selectionModePlug(); + const Gaffer::IntPlug *selectionModePlug() const; + + Gaffer::Int64VectorDataPlug *idListPlug(); + const Gaffer::Int64VectorDataPlug *idListPlug() const; + + Gaffer::StringPlug *idListVariablePlug(); + const Gaffer::StringPlug *idListVariablePlug() const; + + Gaffer::StringPlug *idPlug(); + const Gaffer::StringPlug *idPlug() const; + + Gaffer::StringPlug *maskVariablePlug(); + const Gaffer::StringPlug *maskVariablePlug() const; + + Gaffer::BoolPlug *ignoreMissingPlug(); + const Gaffer::BoolPlug *ignoreMissingPlug() const; + + Gaffer::TweaksPlug *tweaksPlug(); + const Gaffer::TweaksPlug *tweaksPlug() const; + + protected : + + bool affectsProcessedObject( const Gaffer::Plug *input ) const override; + void hashProcessedObject( const ScenePath &path, const Gaffer::Context *context, IECore::MurmurHash &h ) const override; + IECore::ConstObjectPtr computeProcessedObject( const ScenePath &path, const Gaffer::Context *context, const IECore::Object *inputObject ) const override; + + bool adjustBounds() const override; + + private : + + static size_t g_firstPlugIndex; + +}; + +IE_CORE_DECLAREPTR( PrimitiveVariableTweaks ) + +} // namespace GafferScene diff --git a/include/GafferScene/TypeIds.h b/include/GafferScene/TypeIds.h index 890bf4333e5..c8f4ec9a9e7 100644 --- a/include/GafferScene/TypeIds.h +++ b/include/GafferScene/TypeIds.h @@ -90,7 +90,7 @@ enum TypeId PruneTypeId = 110546, FreezeTransformTypeId = 110547, MeshDistortionTypeId = 110548, - OpenGLRenderTypeId = 110549, // Available for reuse + PrimitiveVariableTweaksTypeId = 110549, InteractiveRenderTypeId = 110550, CubeTypeId = 110551, SphereTypeId = 110552, diff --git a/python/GafferSceneTest/PrimitiveVariableTweaksTest.py b/python/GafferSceneTest/PrimitiveVariableTweaksTest.py new file mode 100644 index 00000000000..d3bd7eb21d6 --- /dev/null +++ b/python/GafferSceneTest/PrimitiveVariableTweaksTest.py @@ -0,0 +1,864 @@ +########################################################################## +# +# Copyright (c) 2024, Image Engine Design Inc. 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 imath + +import IECore +import IECoreScene + +import Gaffer +import GafferScene +import GafferSceneTest + +class PrimitiveVariableTweaksTest( GafferSceneTest.SceneTestCase ): + + def typesData( self ): + return { + "a" : IECore.IntData( 10 ), + "b" : IECore.V3fData( imath.V3f( 3 )), + "c" : IECore.V3fData( imath.V3f( 5 ), IECore.GeometricData.Interpretation.Point ), + "d" : IECore.V3fData( imath.V3f( 7 ), IECore.GeometricData.Interpretation.Normal ), + "e" : IECore.Color3fData( imath.Color3f( 0.7, 0.8, 0.6 ) ), + "f" : IECore.Color4fData( imath.Color4f( 0.1, 0.2, 0.3, 0.4 ) ), + "g" : IECore.StringData( "hello to a" ), + "h" : IECore.FloatVectorData( [ 3, 4, 5 ] ), + "i" : IECore.Color3fVectorData( [ imath.Color3f( 7 ) ] ), + } + + def typesCreator( self ): + + create = GafferScene.PrimitiveVariableTweaks() + create["sphere"] = GafferScene.Sphere() + create["in"].setInput( create["sphere"]["out"] ) + + for ( name, val ) in self.typesData().items(): + create["tweaks"].addChild( Gaffer.TweakPlug( name, val, Gaffer.TweakPlug.Mode.Create ) ) + + create["pathFilter"] = GafferScene.PathFilter() + create["pathFilter"]["paths"].setValue( IECore.StringVectorData( [ "/sphere" ] ) ) + create["filter"].setInput( create["pathFilter"]["out"] ) + + return create + + def testNoFilterPassThrough( self ): + create = self.typesCreator() + + create["filter"].setInput( None ) + self.assertScenesEqual( create["sphere"]["out"], create["out"] ) + self.assertSceneHashesEqual( create["sphere"]["out"], create["out"] ) + + def testCreateConstants( self ): + + create = self.typesCreator() + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot create primitive variable "a" when interpolation is set to `Any`. Please select an interpolation.' ): + create["out"].object( "/sphere" ) + + create["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Constant ) + o = create["out"].object( "/sphere" ) + + testData = self.typesData() + self.assertEqual( o.keys(), ['N', 'P'] + list( testData.keys() ) + ['uv'] ) + for k in testData.keys(): + self.assertEqual( o[k], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, testData[k] ) ) + + def testCreateDifferentInterpolations( self ): + + create = self.typesCreator() + create["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Vertex ) + with self.assertRaisesRegex( Gaffer.ProcessException, 'Invalid type "FloatVectorData" for non-constant element-wise tweak "h".' ): + create["out"].object( "/sphere" ) + + create["tweaks"][-1]["enabled"].setValue( False ) + create["tweaks"][-2]["enabled"].setValue( False ) + + testData = self.typesData() + del testData["h"] + del testData["i"] + + for interp in [ + IECoreScene.PrimitiveVariable.Interpolation.Uniform, + IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECoreScene.PrimitiveVariable.Interpolation.Varying, + IECoreScene.PrimitiveVariable.Interpolation.FaceVarying + ]: + create["interpolation"].setValue( interp ) + o = create["out"].object( "/sphere" ) + self.assertEqual( o.keys(), ['N', 'P'] + list( testData.keys() ) + ['uv'] ) + for k in testData.keys(): + dataType = getattr( IECore, testData[k].typeName().replace( "Data", "VectorData" ) ) + compData = dataType( [ testData[k].value ] * o.variableSize( interp ) ) + if hasattr( compData, "setInterpretation" ): + compData.setInterpretation( testData[k].getInterpretation() ) + with self.subTest( interpolation = interp, name = k ): + self.assertEqual( o[k], IECoreScene.PrimitiveVariable( interp, compData ) ) + + def testBadTweakMessages( self ): + + create = self.typesCreator() + create["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Vertex ) + create["tweaks"][-1]["enabled"].setValue( False ) + create["tweaks"][-2]["enabled"].setValue( False ) + + tweak = GafferScene.PrimitiveVariableTweaks() + tweak["in"].setInput( create["out"] ) + tweak["filter"].setInput( create["pathFilter"]["out"] ) + + tweak["tweaks"].addChild( Gaffer.TweakPlug( "a", IECore.Color4fData(), Gaffer.TweakPlug.Mode.Replace ) ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak to "a" : Parameter should be of type "IntData" in order to apply to an element of "IntVectorData", but got "Color4fData" instead.' ): + tweak["out"].object( "/sphere" ) + + create["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Constant ) + + # \todo - this message should end with a period + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak to "a" : Value of type "IntData" does not match parameter of type "Color4fData"' ): + tweak["out"].object( "/sphere" ) + + def tweakData( self ): + return { + "a" : IECore.IntData( 100 ), + "b" : IECore.V3fData( imath.V3f( 0.5 ) ), + "c" : IECore.V3fData( imath.V3f( 0.5 ) ), + "d" : IECore.V3fData( imath.V3f( 0.5 ) ), + "e" : IECore.Color3fData( imath.Color3f( 0.01, 0.02, 0.03 ) ), + "f" : IECore.Color4fData( imath.Color4f( 0.001, 0.002, 0.003, 0.004 ) ), + "g" : IECore.StringData( "to a world" ), + "h" : IECore.FloatVectorData( [ 13, 14, 15 ] ), + "i" : IECore.Color3fVectorData( [ imath.Color3f( 3 ) ] ), + } + + def testReplace( self ): + + create = self.typesCreator() + create["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Constant ) + + tweak = GafferScene.PrimitiveVariableTweaks() + tweak["in"].setInput( create["out"] ) + tweak["filter"].setInput( create["pathFilter"]["out"] ) + + tweakData = self.tweakData() + + for ( name, val ) in tweakData.items(): + tweak["tweaks"].addChild( Gaffer.TweakPlug( name, val, Gaffer.TweakPlug.Mode.Replace ) ) + + typesData = self.typesData() + o = tweak["out"].object( "/sphere" ) + self.assertEqual( o.keys(), ['N', 'P'] + list( typesData.keys() ) + ['uv'] ) + for k in tweakData.keys(): + if not k in [ "c", "d" ]: + self.assertEqual( o[k], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, tweakData[k] ) ) + else: + # When replacing the value of a primvar, we keep the interpolation of the original + self.assertEqual( o[k], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.V3fData( imath.V3f( 0.5 ), typesData[k].getInterpretation() ) ) ) + + def expectedAddData( self ): + return { + "a" : IECore.IntData( 110 ), + "b" : IECore.V3fData( imath.V3f( 3.5 )), + "c" : IECore.V3fData( imath.V3f( 5.5 ), IECore.GeometricData.Interpretation.Point ), + "d" : IECore.V3fData( imath.V3f( 7.5 ), IECore.GeometricData.Interpretation.Normal ), + "e" : IECore.Color3fData( imath.Color3f( 0.71, 0.82, 0.63 ) ), + "f" : IECore.Color4fData( imath.Color4f( 0.101, 0.202, 0.303, 0.404 ) ), + "g" : IECore.StringData( "hello to a world" ), + "h" : IECore.FloatVectorData( [ 3, 4, 5, 13, 14, 15 ] ), + "i" : IECore.Color3fVectorData( [ imath.Color3f( 7 ), imath.Color3f( 3 ) ] ), + } + + def testAddConstant( self ): + + create = self.typesCreator() + create["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Constant ) + + tweak = GafferScene.PrimitiveVariableTweaks() + tweak["in"].setInput( create["out"] ) + tweak["filter"].setInput( create["pathFilter"]["out"] ) + + tweakData = self.tweakData() + + for ( name, val ) in tweakData.items(): + tweak["tweaks"].addChild( Gaffer.TweakPlug( name, val, Gaffer.TweakPlug.Mode.Add ) ) + + expectedAdd = self.expectedAddData() + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak with mode Add to "g" : Data type StringData not supported.' ): + tweak["out"].object( "/sphere" ) + + tweak["tweaks"][-3]["mode"].setValue( Gaffer.TweakPlug.Mode.ListAppend ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak with mode Add to "h" : Data type FloatVectorData not supported.' ): + tweak["out"].object( "/sphere" ) + + tweak["tweaks"][-2]["mode"].setValue( Gaffer.TweakPlug.Mode.ListAppend ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak with mode Add to "i" : Data type Color3fVectorData not supported.' ): + tweak["out"].object( "/sphere" ) + + tweak["tweaks"][-1]["mode"].setValue( Gaffer.TweakPlug.Mode.ListAppend ) + + o = tweak["out"].object( "/sphere" ) + for k in tweakData.keys(): + self.assertEqual( o[k], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, expectedAdd[k] ) ) + + + def testReplaceVertex( self ): + + create = self.typesCreator() + create["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Vertex ) + + tweak = GafferScene.PrimitiveVariableTweaks() + tweak["in"].setInput( create["out"] ) + tweak["filter"].setInput( create["pathFilter"]["out"] ) + + tweakData = self.tweakData() + del tweakData["h"] + del tweakData["i"] + + for ( name, val ) in tweakData.items(): + tweak["tweaks"].addChild( Gaffer.TweakPlug( name, val, Gaffer.TweakPlug.Mode.Replace ) ) + + create["tweaks"][-1]["enabled"].setValue( False ) + create["tweaks"][-2]["enabled"].setValue( False ) + + typesData = self.typesData() + + for interp in [ + IECoreScene.PrimitiveVariable.Interpolation.Uniform, + IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECoreScene.PrimitiveVariable.Interpolation.Varying, + IECoreScene.PrimitiveVariable.Interpolation.FaceVarying + ]: + create["interpolation"].setValue( interp ) + o = tweak["out"].object( "/sphere" ) + self.assertEqual( o.keys(), ['N', 'P'] + list( tweakData.keys() ) + ['uv'] ) + for k in tweakData.keys(): + dataType = getattr( IECore, tweakData[k].typeName().replace( "Data", "VectorData" ) ) + compData = dataType( [ tweakData[k].value ] * o.variableSize( interp ) ) + if hasattr( compData, "setInterpretation" ): + compData.setInterpretation( typesData[k].getInterpretation() ) + self.assertEqual( o[k], IECoreScene.PrimitiveVariable( interp, compData ) ) + + def testAddVertex( self ): + + create = self.typesCreator() + create["tweaks"][-1]["enabled"].setValue( False ) + create["tweaks"][-2]["enabled"].setValue( False ) + + tweak = GafferScene.PrimitiveVariableTweaks() + tweak["in"].setInput( create["out"] ) + tweak["filter"].setInput( create["pathFilter"]["out"] ) + + tweakData = self.tweakData() + del tweakData["h"] + del tweakData["i"] + + for ( name, val ) in tweakData.items(): + tweak["tweaks"].addChild( Gaffer.TweakPlug( name, val, Gaffer.TweakPlug.Mode.Add ) ) + + # listAppend mode works on string even when those strings are per-vertex + tweak["tweaks"][-1]["mode"].setValue( Gaffer.TweakPlug.Mode.ListAppend ) + + expectedAdd = self.expectedAddData() + del expectedAdd["h"] + del expectedAdd["i"] + + + for interp in [ + IECoreScene.PrimitiveVariable.Interpolation.Uniform, + IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECoreScene.PrimitiveVariable.Interpolation.Varying, + IECoreScene.PrimitiveVariable.Interpolation.FaceVarying + ]: + create["interpolation"].setValue( interp ) + o = tweak["out"].object( "/sphere" ) + self.assertEqual( o.keys(), ['N', 'P'] + list( expectedAdd.keys() ) + ['uv'] ) + for k in expectedAdd.keys(): + dataType = getattr( IECore, expectedAdd[k].typeName().replace( "Data", "VectorData" ) ) + compData = dataType( [ expectedAdd[k].value ] * o.variableSize( interp ) ) + if hasattr( compData, "setInterpretation" ): + compData.setInterpretation( expectedAdd[k].getInterpretation() ) + self.assertEqual( o[k], IECoreScene.PrimitiveVariable( interp, compData ) ) + + def testInvalidPrimVar( self ): + + m = IECoreScene.MeshPrimitive.createPlane( imath.Box2f( imath.V2f( -1 ), imath.V2f( 1 ) ), imath.V2i( 2 ) ) + m["a"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECore.IntVectorData( [ 0, 1, 2 ] ) + ) + + p = GafferScene.ObjectToScene() + p["object"].setValue( m ) + + f = GafferScene.PathFilter() + f["paths"].setValue( IECore.StringVectorData( [ "/object" ] ) ) + + badTweak = GafferScene.PrimitiveVariableTweaks() + badTweak["in"].setInput( p["out"] ) + badTweak["filter"].setInput( f["out"] ) + badTweak["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Constant ) + badTweak["tweaks"].addChild( Gaffer.TweakPlug( "a", IECore.IntData( 7 ), Gaffer.TweakPlug.Mode.Create ) ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot tweak "a" : Primitive variable not valid.' ): + badTweak["out"].object( "/object" ) + + def testInvalidId( self ): + + m = IECoreScene.MeshPrimitive.createPlane( imath.Box2f( imath.V2f( -1 ), imath.V2f( 1 ) ), imath.V2i( 2 ) ) + m["badIds"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECore.IntVectorData( [ 0, 1 ] ) + ) + + p = GafferScene.ObjectToScene() + p["object"].setValue( m ) + + f = GafferScene.PathFilter() + f["paths"].setValue( IECore.StringVectorData( [ "/object" ] ) ) + + badTweak = GafferScene.PrimitiveVariableTweaks() + badTweak["in"].setInput( p["out"] ) + badTweak["filter"].setInput( f["out"] ) + badTweak["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Vertex ) + badTweak["tweaks"].addChild( Gaffer.TweakPlug( "P", IECore.V3fData( imath.V3f( 7 ) ), Gaffer.TweakPlug.Mode.Replace ) ) + badTweak["selectionMode"].setValue( GafferScene.PrimitiveVariableTweaks.SelectionMode.IdList ) + badTweak["idList"].setValue( IECore.Int64VectorData( [ 1, 2] ) ) + badTweak["id"].setValue( "badIds" ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Id primitive variable "badIds" is not valid.' ): + badTweak["out"].object( "/object" ) + + m["badIds"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECore.IntVectorData( [ 0, 1 ] ), IECore.IntVectorData( [ 0, 0, 1, 1, 0, 0, 0, 1, 0 ] ) + ) + p["object"].setValue( m ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Id variable "badIds" is not allowed to be indexed.' ): + badTweak["out"].object( "/object" ) + + def createConstantsAndUniforms( self ): + m = IECoreScene.MeshPrimitive.createPlane( imath.Box2f( imath.V2f( -1 ), imath.V2f( 1 ) ), imath.V2i( 2 ) ) + m["vertexIds"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECore.IntVectorData( [ 10, 11, 12, 20, 21, 21, 30, 31, 33 ] ) + ) + m["indexedMask"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECore.IntVectorData( [ 0, 1 ] ), IECore.IntVectorData( [ 0, 0, 1, 1, 0, 0, 0, 1, 0 ] ) + ) + + result = {} + result["objectToScene"] = GafferScene.ObjectToScene() + result["objectToScene"]["name"].setValue( "plane" ) + result["objectToScene"]["object"].setValue( m ) + + result["filter"] = GafferScene.PathFilter() + result["filter"]["paths"].setValue( IECore.StringVectorData( [ "/plane" ] ) ) + + result["constants"] = GafferScene.PrimitiveVariableTweaks() + result["constants"]["in"].setInput( result["objectToScene"]["out"] ) + result["constants"]["filter"].setInput( result["filter"]["out"] ) + result["constants"]["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Constant ) + result["constants"]["tweaks"].addChild( Gaffer.TweakPlug( "a", IECore.FloatData( 7 ), Gaffer.TweakPlug.Mode.Create ) ) + result["constants"]["tweaks"].addChild( Gaffer.TweakPlug( "c", IECore.IntVectorData( [ 3, 4, 8 ] ), Gaffer.TweakPlug.Mode.Create ) ) + + result["result"] = GafferScene.PrimitiveVariableTweaks() + result["result"]["in"].setInput( result["constants"]["out"] ) + result["result"]["filter"].setInput( result["filter"]["out"] ) + result["result"]["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Uniform ) + result["result"]["tweaks"].addChild( Gaffer.TweakPlug( "b", IECore.IntData( 42 ), Gaffer.TweakPlug.Mode.Create ) ) + + result["result"]["selectionMode"].setValue( GafferScene.PrimitiveVariableTweaks.SelectionMode.IdList ) + result["result"]["idList"].setValue( IECore.Int64VectorData( [ 1, 2] ) ) + + return result + + def testCreateConstantsAndUniforms( self ): + + create = self.createConstantsAndUniforms() + + o = create["result"]["out"].object( "/plane" ) + + self.assertEqual( o["a"].data, IECore.FloatData( 7 ) ) + self.assertEqual( o["b"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Uniform, IECore.IntVectorData( [ 0, 42, 42, 0 ] ) ) ) + self.assertEqual( o["c"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.IntVectorData( [ 3, 4, 8 ] ) ) ) + + def testInvalidTweaks( self ): + create = self.createConstantsAndUniforms() + + tweak = GafferScene.PrimitiveVariableTweaks() + tweak["in"].setInput( create["result"]["out"] ) + tweak["filter"].setInput( create["filter"]["out"] ) + tweak["tweaks"].addChild( Gaffer.TweakPlug( "a", IECore.FloatData( 42 ), Gaffer.TweakPlug.Mode.Replace ) ) + + self.assertEqual( tweak["out"].object( "/plane" )["a"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.FloatData( 42 ) ) ) + + tweak["tweaks"][0]["name"].setValue( "x" ) + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot find primitive variable "x" to tweak.' ): + tweak["out"].object( "/plane" ) + + tweak["ignoreMissing"].setValue( True ) + self.assertEqual( tweak["out"].object( "/plane" ), tweak["in"].object( "/plane" ) ) + + tweak["tweaks"][0]["name"].setValue( "a" ) + tweak["tweaks"][0]["mode"].setValue( Gaffer.TweakPlug.Mode.ListAppend ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak with mode ListAppend to "a" : Data type FloatData not supported.' ): + tweak["out"].object( "/plane" ) + + def testInvalidVectorTweaks( self ): + + create = self.createConstantsAndUniforms() + + tweak = GafferScene.PrimitiveVariableTweaks() + tweak["in"].setInput( create["result"]["out"] ) + tweak["filter"].setInput( create["filter"]["out"] ) + tweak["tweaks"].addChild( Gaffer.TweakPlug( "x", IECore.IntData( 7 ), Gaffer.TweakPlug.Mode.Replace ) ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot find primitive variable "x" to tweak.' ): + tweak["out"].object( "/plane" ) + + del tweak["tweaks"][0] + tweak["tweaks"].addChild( Gaffer.TweakPlug( "x", IECore.FloatVectorData( [ 7 ] ), Gaffer.TweakPlug.Mode.ListAppend ) ) + + # Applying a ListAppend tweak when there is no source found will try to create the variable, which will + # fail if the interpolation isn't set + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot create primitive variable "x" when interpolation is set to `Any`. Please select an interpolation.' ): + tweak["out"].object( "/plane" ) + + tweak["ignoreMissing"].setValue( True ) + + # This error is not affected by ignoreMissing + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot create primitive variable "x" when interpolation is set to `Any`. Please select an interpolation.' ): + tweak["out"].object( "/plane" ) + tweak["ignoreMissing"].setValue( False ) + + tweak["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Constant ) + self.assertEqual( tweak["out"].object( "/plane" )["x"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.FloatVectorData( [ 7 ] ) ) ) + + tweak["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Vertex ) + with self.assertRaisesRegex( Gaffer.ProcessException, 'Invalid type "FloatVectorData" for non-constant element-wise tweak "x".' ): + tweak["out"].object( "/plane" )["x"] + + + def testListTweaks( self ): + create = self.createConstantsAndUniforms() + + tweak = GafferScene.PrimitiveVariableTweaks() + tweak["in"].setInput( create["result"]["out"] ) + tweak["filter"].setInput( create["filter"]["out"] ) + tweak["tweaks"].addChild( Gaffer.TweakPlug( "c", IECore.IntVectorData( [ 7 ] ), Gaffer.TweakPlug.Mode.ListAppend ) ) + + # List append working as intended + self.assertEqual( tweak["out"].object( "/plane" )["c"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.IntVectorData( [ 3, 4, 8, 7 ] ) ) ) + + # List remove is considered successful if the target doesn't exist + tweak["tweaks"][0]["name"].setValue( "x" ) + tweak["tweaks"][0]["mode"].setValue( Gaffer.TweakPlug.Mode.ListRemove ) + self.assertEqual( tweak["out"].object( "/plane" ), tweak["in"].object( "/plane" ) ) + + # List remove working as intended + tweak["tweaks"][0]["name"].setValue( "c" ) + tweak["tweaks"][0]["value"].setValue( IECore.IntVectorData( [ 4 ] ) ) + self.assertEqual( tweak["out"].object( "/plane" )["c"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.IntVectorData( [ 3, 8 ] ) ) ) + + def testCreateEdgeCases( self ): + create = self.createConstantsAndUniforms() + + tweak = GafferScene.PrimitiveVariableTweaks() + tweak["in"].setInput( create["result"]["out"] ) + tweak["filter"].setInput( create["filter"]["out"] ) + tweak["tweaks"].addChild( Gaffer.TweakPlug( "P", IECore.V3fData( imath.V3f( 42 ), IECore.GeometricData.Interpretation.Point ), Gaffer.TweakPlug.Mode.CreateIfMissing ) ) + + # CreateIfMissing does nothing if there's already something there + self.assertEqual( tweak["out"].object( "/plane" ), tweak["in"].object( "/plane" ) ) + + # It doesn't matter if the type is wrong + del tweak["tweaks"][0] + tweak["tweaks"].addChild( Gaffer.TweakPlug( "P", IECore.BoolData( True ), Gaffer.TweakPlug.Mode.CreateIfMissing ) ) + self.assertEqual( tweak["out"].object( "/plane" ), tweak["in"].object( "/plane" ) ) + + # Or the interpolation is wrong + tweak["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Uniform ) + self.assertEqual( tweak["out"].object( "/plane" ), tweak["in"].object( "/plane" ) ) + + del tweak["tweaks"][0] + tweak["tweaks"].addChild( Gaffer.TweakPlug( "Px", IECore.V3fData( imath.V3f( 42 ), IECore.GeometricData.Interpretation.Point ), Gaffer.TweakPlug.Mode.CreateIfMissing ) ) + tweak["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Vertex ) + o = tweak["out"].object( "/plane" ) + self.assertEqual( o["Px"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ imath.V3f( 42 ) ] * 9, IECore.GeometricData.Interpretation.Point ) ) ) + + # A very weird corner case - ListAppend with a V3f is totally bogus ... but if there is no existing + # value, ListAppend is treated as a Create, which succeeds. + tweak["tweaks"][0]["mode"].setValue( Gaffer.TweakPlug.Mode.ListAppend ) + o = tweak["out"].object( "/plane" ) + self.assertEqual( o["Px"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ imath.V3f( 42 ) ] * 9, IECore.GeometricData.Interpretation.Point ) ) ) + + # If there is already a variable there, we get the expected error + tweak["tweaks"][0]["name"].setValue( "P" ) + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak with mode ListAppend to "P" : Data type V3fDataBase not supported.' ): + tweak["out"].object( "/plane" ) + + # We can use Create to overwrite a variable with a completely different type + del tweak["tweaks"][0] + tweak["tweaks"].addChild( Gaffer.TweakPlug( "P", IECore.StringData( "foo" ), Gaffer.TweakPlug.Mode.Create ) ) + + o = tweak["out"].object( "/plane" ) + self.assertEqual( o["P"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.StringVectorData( [ "foo" ] * 9 ) ) ) + + # Or use Create to overwrite a variable with a completely different interpolation + tweak["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Uniform ) + o = tweak["out"].object( "/plane" ) + self.assertEqual( o["P"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Uniform, IECore.StringVectorData( [ "foo" ] * 4 ) ) ) + + def createVariousTweaks( self, mode ): + tweak = GafferScene.PrimitiveVariableTweaks() + tweak["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Invalid ) + + tweak["tweaks"].addChild( Gaffer.TweakPlug( "P", IECore.V3fData( imath.V3f( 0.5 ) ), mode ) ) + tweak["tweaks"].addChild( Gaffer.TweakPlug( "N", IECore.V3fData( imath.V3f( 0.1 ) ), mode ) ) + tweak["tweaks"].addChild( Gaffer.TweakPlug( "uv", IECore.V2fData( imath.V2f( 10 ) ), mode ) ) + tweak["tweaks"].addChild( Gaffer.TweakPlug( "a", IECore.FloatData( 0.7 ), mode ) ) + tweak["tweaks"].addChild( Gaffer.TweakPlug( "b", IECore.IntData( 7 ), mode ) ) + tweak["tweaks"].addChild( Gaffer.TweakPlug( "c", IECore.IntVectorData( [7] ), mode ) ) + + return tweak + + def testReplaceVariousInterps( self ): + create = self.createConstantsAndUniforms() + tweak = self.createVariousTweaks( Gaffer.TweakPlug.Mode.Replace ) + tweak["in"].setInput( create["result"]["out"] ) + tweak["filter"].setInput( create["filter"]["out"] ) + + uvIndices = tweak["in"].object( "/plane" )["uv"].indices + + o = tweak["out"].object( "/plane" ) + self.assertEqual( o.keys(), [ "N", "P", "a", "b", "c", "indexedMask", "uv", "vertexIds" ] ) + self.assertEqual( o["N"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ imath.V3f( 0.1 ) ] * 9, IECore.GeometricData.Interpretation.Normal ) ) ) + self.assertEqual( o["P"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ imath.V3f( 0.5 ) ] * 9, IECore.GeometricData.Interpretation.Point ) ) ) + self.assertEqual( o["a"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.FloatData( 0.7 ) ) ) + self.assertEqual( o["b"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Uniform, IECore.IntVectorData( [ 7 ] * 4 ) ) ) + self.assertEqual( o["c"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.IntVectorData( [ 7 ] ) ) ) + self.assertEqual( o["uv"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.FaceVarying, IECore.V2fVectorData( [ imath.V2f( 10 ) ] * 9, IECore.GeometricData.Interpretation.UV ), uvIndices ) ) + + def testRemoveVariousInterps( self ): + create = self.createConstantsAndUniforms() + tweak = self.createVariousTweaks( Gaffer.TweakPlug.Mode.Remove ) + tweak["in"].setInput( create["result"]["out"] ) + tweak["filter"].setInput( create["filter"]["out"] ) + + # Here, all the types match nicely + o = tweak["out"].object( "/plane" ) + self.assertEqual( o.keys(), [ "indexedMask", "vertexIds" ] ) + + # But it wouldn't matter if we gave a completely wrong type when doing a remove + del tweak["tweaks"][-1] + tweak["tweaks"].addChild( Gaffer.TweakPlug( "c", IECore.StringData( "foo" ), Gaffer.TweakPlug.Mode.Remove ) ) + + o = tweak["out"].object( "/plane" ) + self.assertEqual( o.keys(), [ "indexedMask", "vertexIds" ] ) + + # Currently, though, we do throw an error if the interpolation is wrong + tweak["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Vertex ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak to "uv" : Interpolation `Vertex` doesn\'t match primitive variable interpolation `FaceVarying`.' ): + tweak["out"].object( "/plane" ) + + def testAddVariousInterps( self ): + create = self.createConstantsAndUniforms() + tweak = self.createVariousTweaks( Gaffer.TweakPlug.Mode.Add ) + tweak["in"].setInput( create["result"]["out"] ) + tweak["filter"].setInput( create["filter"]["out"] ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak with mode Add to "c" : Data type IntVectorData not supported.' ): + tweak["out"].object( "/plane" ) + + tweak["tweaks"][-1]["mode"].setValue( Gaffer.TweakPlug.Mode.ListAppend ) + + uvIndices = tweak["in"].object( "/plane" )["uv"].indices + o = tweak["out"].object( "/plane" ) + self.assertEqual( o.keys(), [ "N", "P", "a", "b", "c", "indexedMask", "uv", "vertexIds" ] ) + self.assertEqual( o["N"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ imath.V3f( 0.1, 0.1, 1.1 ) ] * 9, IECore.GeometricData.Interpretation.Normal ) ) ) + self.assertEqual( o["P"], IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( + [ imath.V3f( x + 0.5, y + 0.5, 0.5 ) for x, y in + [ (-1, -1), (0, -1), (1, -1), ( -1, 0 ), ( 0, 0 ), (1, 0 ), ( -1, 1 ), ( 0, 1 ), ( 1, 1 ) ] ], + IECore.GeometricData.Interpretation.Point ) ) ) + self.assertEqual( o["a"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.FloatData( 7.7 ) ) ) + self.assertEqual( o["b"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Uniform, IECore.IntVectorData( [ 7, 49, 49, 7 ] ) ) ) + self.assertEqual( o["c"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.IntVectorData( [ 3, 4, 8, 7 ] ) ) ) + self.assertEqual( o["uv"], IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.FaceVarying, IECore.V2fVectorData( + [ imath.V2f( x + 10, y + 10 ) for x, y in + [ (0, 0), (0.5, 0), (1, 0), ( 0, 0.5 ), ( 0.5, 0.5 ), (1, 0.5 ), ( 0, 1 ), ( 0.5, 1 ), ( 1, 1 ) ] ], + IECore.GeometricData.Interpretation.UV ), uvIndices ) ) + + + def testInvalidVertexTweaks( self ): + + create = self.createConstantsAndUniforms() + tweak = self.createVariousTweaks( Gaffer.TweakPlug.Mode.Add ) + tweak["in"].setInput( create["result"]["out"] ) + tweak["filter"].setInput( create["filter"]["out"] ) + + tweak["tweaks"][-1]["mode"].setValue( Gaffer.TweakPlug.Mode.ListAppend ) + + o = tweak["out"].object( "/plane" ) + + # Setting the selection mode does nothing while the interpolation is set to `Any` + tweak["selectionMode"].setValue( GafferScene.PrimitiveVariableTweaks.SelectionMode.IdList ) + tweak["idList"].setValue( IECore.Int64VectorData( [ 0, 3, 4, 5 ] ) ) + + self.assertEqual( tweak["out"].object( "/plane" ), o ) + + tweak["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Vertex ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak to "uv" : Interpolation `Vertex` doesn\'t match primitive variable interpolation `FaceVarying`.' ): + tweak["out"].object( "/plane" ) + + tweak["tweaks"][2]["enabled"].setValue( False ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak to "a" : Interpolation `Vertex` doesn\'t match primitive variable interpolation `Constant`.' ): + tweak["out"].object( "/plane" ) + + tweak["tweaks"][3]["enabled"].setValue( False ) + tweak["tweaks"][5]["enabled"].setValue( False ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak to "b" : Interpolation `Vertex` doesn\'t match primitive variable interpolation `Uniform`.' ): + tweak["out"].object( "/plane" ) + + def testVertexIdList( self ): + + create = self.createConstantsAndUniforms() + tweak = GafferScene.PrimitiveVariableTweaks() + tweak["in"].setInput( create["result"]["out"] ) + tweak["filter"].setInput( create["filter"]["out"] ) + + tweak["tweaks"].addChild( Gaffer.TweakPlug( "P", IECore.V3fData( imath.V3f( 0.5 ) ), Gaffer.TweakPlug.Mode.Add ) ) + tweak["tweaks"].addChild( Gaffer.TweakPlug( "N", IECore.V3fData( imath.V3f( 0.1 ) ), Gaffer.TweakPlug.Mode.Add ) ) + + tweak["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Vertex ) + tweak["selectionMode"].setValue( GafferScene.PrimitiveVariableTweaks.SelectionMode.IdList ) + tweak["idList"].setValue( IECore.Int64VectorData( [ 0, 3, 4, 5 ] ) ) + + refObj = tweak["in"].object( "/plane" ) + refObj["N"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ imath.V3f( *i ) for i in [ + (0.1, 0.1, 1.1), (0, 0, 1), (0, 0, 1), + (0.1, 0.1, 1.1), (0.1, 0.1, 1.1), (0.1, 0.1, 1.1), + (0, 0, 1), (0, 0, 1), (0, 0, 1) + ] ], IECore.GeometricData.Interpretation.Normal ) ) + refObj["P"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( + [ imath.V3f( *i ) for i in + [ (-0.5, -0.5, 0.5), (0, -1, 0), (1, -1, 0), ( -0.5, 0.5, 0.5 ), ( 0.5, 0.5, 0.5 ), (1.5, 0.5, 0.5 ), ( -1, 1, 0 ), ( 0, 1, 0 ), ( 1, 1, 0 ) ] ], + IECore.GeometricData.Interpretation.Point ) ) + self.assertEqual( tweak["out"].object( "/plane" ), refObj ) + + # Check that an id appearing twice in the id list has no extra effect. + tweak["idList"].setValue( IECore.Int64VectorData( [ 0, 3, 4, 5, 3 ] ) ) + o = tweak["out"].object( "/plane" ) + self.assertEqual( o["N"], IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ imath.V3f( *i ) for i in [ + (0.1, 0.1, 1.1), (0, 0, 1), (0, 0, 1), + (0.1, 0.1, 1.1), (0.1, 0.1, 1.1), (0.1, 0.1, 1.1), + (0, 0, 1), (0, 0, 1), (0, 0, 1) + ] ], IECore.GeometricData.Interpretation.Normal ) ) ) + + # Test IdListPrimVarMode + tweak["selectionMode"].setValue( GafferScene.PrimitiveVariableTweaks.SelectionMode.IdListPrimitiveVariable ) + tweak["idListVariable"].setValue( "bad" ) + with self.assertRaisesRegex( Gaffer.ProcessException, 'Can\'t find id list primitive variable "bad".' ): + tweak["out"].object( "/plane" ) + tweak["idListVariable"].setValue( "a" ) + with self.assertRaisesRegex( Gaffer.ProcessException, 'Invalid id list primitive variable "a". A constant IntVector or Int64Vector is required.' ): + tweak["out"].object( "/plane" ) + tweak["idListVariable"].setValue( "vertexIds" ) + with self.assertRaisesRegex( Gaffer.ProcessException, 'Invalid id list primitive variable "vertexIds". A constant IntVector or Int64Vector is required.' ): + tweak["out"].object( "/plane" ) + + tweak["idListVariable"].setValue( "c" ) + o = tweak["out"].object( "/plane" ) + self.assertEqual( o["N"], IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ imath.V3f( *i ) for i in [ + (0, 0, 1), (0, 0, 1), (0, 0, 1), + (0.1, 0.1, 1.1), (0.1, 0.1, 1.1), (0, 0, 1), + (0, 0, 1), (0, 0, 1), (0.1, 0.1, 1.1) + ] ], IECore.GeometricData.Interpretation.Normal ) ) ) + + tweak["id"].setValue( "vertexIds" ) + + # The current id list doesn't match these new ids + self.assertEqual( tweak["out"].object( "/plane" ), tweak["in"].object( "/plane" ) ) + + tweak["selectionMode"].setValue( GafferScene.PrimitiveVariableTweaks.SelectionMode.IdList ) + tweak["idList"].setValue( IECore.Int64VectorData( [ 11, 31 ] ) ) + + refObj["N"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ imath.V3f( *i ) for i in [ + (0, 0, 1), (0.1, 0.1, 1.1), (0, 0, 1), + (0, 0, 1), (0, 0, 1), (0, 0, 1), + (0, 0, 1), (0.1, 0.1, 1.1), (0, 0, 1) + ] ], IECore.GeometricData.Interpretation.Normal ) ) + refObj["P"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( + [ imath.V3f( *i ) for i in + [ (-1, -1, 0), (0.5, -0.5, 0.5), (1, -1, 0), ( -1, 0, 0 ), ( 0, 0, 0 ), (1, 0, 0 ), ( -1, 1, 0 ), ( 0.5, 1.5, 0.5 ), ( 1, 1, 0 ) ] ], + IECore.GeometricData.Interpretation.Point ) ) + self.assertEqual( tweak["out"].object( "/plane" ), refObj ) + + + def testUniformIdList( self ): + + create = self.createConstantsAndUniforms() + + tweak = GafferScene.PrimitiveVariableTweaks() + tweak["in"].setInput( create["result"]["out"] ) + tweak["filter"].setInput( create["filter"]["out"] ) + tweak["tweaks"].addChild( Gaffer.TweakPlug( "b", IECore.IntData( 7 ), Gaffer.TweakPlug.Mode.Add ) ) + + tweak["selectionMode"].setValue( GafferScene.PrimitiveVariableTweaks.SelectionMode.IdList ) + tweak["idList"].setValue( IECore.Int64VectorData( [ 11, 31 ] ) ) + + tweak["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Uniform ) + + inObj = tweak["in"].object( "/plane" ) + + # Check that only in-bound ids have any effect + self.assertEqual( tweak["out"].object( "/plane" ), inObj ) + + tweak["idList"].setValue( IECore.Int64VectorData( [ 2, 31 ] ) ) + + o = tweak["out"].object( "/plane" ) + self.assertEqual( o["b"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Uniform, IECore.IntVectorData( [ 0, 42, 49, 0 ] ) ) ) + + tweak["selectionMode"].setValue( GafferScene.PrimitiveVariableTweaks.SelectionMode.All ) + o = tweak["out"].object( "/plane" ) + self.assertEqual( o["b"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Uniform, IECore.IntVectorData( [ 7, 49, 49, 7 ] ) ) ) + + tweak["selectionMode"].setValue( GafferScene.PrimitiveVariableTweaks.SelectionMode.IdList ) + tweak["id"].setValue( "vertexIds" ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Id variable "vertexIds" : Interpolation `Vertex` doesn\'t match specified interpolation `Uniform`.' ): + tweak["out"].object( "/plane" ) + + + def testFaceVaryingIndexed( self ): + + create = self.createConstantsAndUniforms() + tweak = GafferScene.PrimitiveVariableTweaks() + tweak["in"].setInput( create["result"]["out"] ) + tweak["filter"].setInput( create["filter"]["out"] ) + tweak["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.FaceVarying ) + tweak["tweaks"].addChild( Gaffer.TweakPlug( "uv", IECore.V2fData( imath.V2f( 10 ) ), Gaffer.TweakPlug.Mode.Add ) ) + + uvIndices = tweak["in"].object( "/plane" )["uv"].indices + refObj = tweak["in"].object( "/plane" ) + refObj["uv"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.FaceVarying, IECore.V2fVectorData( + [ imath.V2f( x + 10, y + 10 ) for x, y in + [ (0, 0), (0.5, 0), (1, 0), ( 0, 0.5 ), ( 0.5, 0.5 ), (1, 0.5 ), ( 0, 1 ), ( 0.5, 1 ), ( 1, 1 ) ] ], + IECore.GeometricData.Interpretation.UV ), uvIndices ) + + self.assertEqual( tweak["out"].object( "/plane" ), refObj ) + + # When tweaking an indexed primvar, things are a bit more complex - any data that gets tweaked gets + # a new index, and any data that no longer has any indices referring to it is removed. + tweak["selectionMode"].setValue( GafferScene.PrimitiveVariableTweaks.SelectionMode.IdList ) + tweak["idList"].setValue( IECore.Int64VectorData( [ 0, 1, 2, 3, 12, 13, 14, 15 ] ) ) + + refObj["uv"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.FaceVarying, IECore.V2fVectorData( + [ imath.V2f( *i ) for i in + [ ( 0.5, 0 ), ( 1, 0 ), ( 0, 0.5 ), ( 0.5, 0.5 ), ( 1, 0.5 ), ( 0, 1 ), ( 0.5, 1 ), ( 10, 10 ), ( 10.5, 10 ), ( 10.5, 10.5 ), ( 10, 10.5 ), ( 11, 10.5 ), ( 11, 11 ), ( 10.5, 11 ) ] ], + IECore.GeometricData.Interpretation.UV ), + IECore.IntVectorData( [ 7, 8, 9, 10, 0, 1, 4, 3, 2, 3, 6, 5, 9, 11, 12, 13 ] ) + ) + self.assertEqual( tweak["out"].object( "/plane" ), refObj ) + + + def testMaskVariable( self ): + + create = self.createConstantsAndUniforms() + tweak = GafferScene.PrimitiveVariableTweaks() + tweak["in"].setInput( create["result"]["out"] ) + tweak["filter"].setInput( create["filter"]["out"] ) + + tweak["tweaks"].addChild( Gaffer.TweakPlug( "N", IECore.V3fData( imath.V3f( 0.1 ) ), Gaffer.TweakPlug.Mode.Add ) ) + + tweak["selectionMode"].setValue( GafferScene.PrimitiveVariableTweaks.SelectionMode.MaskPrimitiveVariable ) + tweak["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Vertex ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Can\'t find mask primitive variable "".' ): + tweak["out"].object( "/plane" ) + + tweak["maskVariable"].setValue( "uv" ) + with self.assertRaisesRegex( Gaffer.ProcessException, 'Mask primitive variable "uv" has wrong interpolation `FaceVarying`, expected `Vertex`.' ): + tweak["out"].object( "/plane" ) + + tweak["maskVariable"].setValue( "indexedMask" ) + o = tweak["out"].object( "/plane" ) + self.assertEqual( o["N"], IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ imath.V3f( *i ) for i in [ + (0, 0, 1), (0, 0, 1), (0.1, 0.1, 1.1), + (0.1, 0.1, 1.1), (0, 0, 1), (0, 0, 1), + (0, 0, 1), (0.1, 0.1, 1.1), (0, 0, 1) + ] ], IECore.GeometricData.Interpretation.Normal ) ) ) + + def testBoundUpdate( self ) : + + sphere = GafferScene.Sphere() + + sphereFilter = GafferScene.PathFilter() + sphereFilter["paths"].setValue( IECore.StringVectorData( [ "/sphere" ] ) ) + + tweak = GafferScene.PrimitiveVariableTweaks() + tweak["in"].setInput( sphere["out"] ) + tweak["filter"].setInput( sphereFilter["out"] ) + + # We don't want to pay for unnecessary bounds propagation if P isn't being updated. + self.assertScenesEqual( tweak["out"], tweak["in"], checks = { "bound" } ) + self.assertSceneHashesEqual( tweak["out"], tweak["in"], checks = { "bound" } ) + self.assertEqual( tweak["out"].bound( "/sphere" ), imath.Box3f( imath.V3f( -1 ), imath.V3f( 1 ) ) ) + + # Try an actual write to P + tweak["tweaks"].addChild( Gaffer.TweakPlug( "P", IECore.V3fData( imath.V3f( 1 ) ), Gaffer.TweakPlug.Mode.Add ) ) + self.assertEqual( tweak["out"].bound( "/sphere" ), imath.Box3f( imath.V3f( 0 ), imath.V3f( 2 ) ) ) + + # Bounds are passed through if name doesn't match + tweak["tweaks"][0]["name"].setValue( "notP" ) + self.assertScenesEqual( tweak["out"], tweak["in"], checks = { "bound" } ) + self.assertSceneHashesEqual( tweak["out"], tweak["in"], checks = { "bound" } ) + self.assertEqual( tweak["out"].bound( "/sphere" ), imath.Box3f( imath.V3f( -1 ), imath.V3f( 1 ) ) ) + +if __name__ == "__main__": + unittest.main() diff --git a/python/GafferSceneUI/PrimitiveVariableTweaksUI.py b/python/GafferSceneUI/PrimitiveVariableTweaksUI.py new file mode 100644 index 00000000000..5855d98f3e9 --- /dev/null +++ b/python/GafferSceneUI/PrimitiveVariableTweaksUI.py @@ -0,0 +1,393 @@ +########################################################################## +# +# Copyright (c) 2024, Image Engine Design Inc. 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 imath +import functools +import collections + +import IECore +import IECoreScene + +import Gaffer +import GafferUI +import GafferScene +import GafferSceneUI + +def __primVarTweaksSelectionModeEnabled( node ): + return not node["interpolation"].getValue() in [ + IECoreScene.PrimitiveVariable.Interpolation.Invalid, IECoreScene.PrimitiveVariable.Interpolation.Constant + ] + + +Gaffer.Metadata.registerNode( + + GafferScene.PrimitiveVariableTweaks, + + "description", + """ + Modify primitive variable values. Supports modifying values just for specific elements of the + primitive. + """, + + "layout:activator:selectionModeEnabled", lambda node : __primVarTweaksSelectionModeEnabled( node ), + "layout:activator:idListExplicitVisible", lambda node : __primVarTweaksSelectionModeEnabled( node ) and node["selectionMode"].getValue() == GafferScene.PrimitiveVariableTweaks.SelectionMode.IdList, + "layout:activator:idListVarVisible", lambda node : __primVarTweaksSelectionModeEnabled( node ) and node["selectionMode"].getValue() == GafferScene.PrimitiveVariableTweaks.SelectionMode.IdListPrimitiveVariable, + "layout:activator:idListVisible", lambda node : __primVarTweaksSelectionModeEnabled( node ) and node["selectionMode"].getValue() in [ GafferScene.PrimitiveVariableTweaks.SelectionMode.IdList, GafferScene.PrimitiveVariableTweaks.SelectionMode.IdListPrimitiveVariable ], + "layout:activator:maskVarVisible", lambda node : __primVarTweaksSelectionModeEnabled( node ) and node["selectionMode"].getValue() == GafferScene.PrimitiveVariableTweaks.SelectionMode.MaskPrimitiveVariable, + + "layout:section:Settings.Tweaks:collapsed", False, + + plugs = { + + "interpolation" : [ + + "description", + """ + The interpolation of the target primitive variables. Using "Any" allows you to + operate on any primitive variable, but if you know your target, using a more + specific interpolation offers benefits: you can specify an idList to operate + on specific elements, and you can use "Create" mode to create new primitive + variables. + """, + + "preset:Any", IECoreScene.PrimitiveVariable.Interpolation.Invalid, + "preset:Constant", IECoreScene.PrimitiveVariable.Interpolation.Constant, + "preset:Uniform", IECoreScene.PrimitiveVariable.Interpolation.Uniform, + "preset:Vertex", IECoreScene.PrimitiveVariable.Interpolation.Vertex, + "preset:Varying", IECoreScene.PrimitiveVariable.Interpolation.Varying, + "preset:FaceVarying", IECoreScene.PrimitiveVariable.Interpolation.FaceVarying, + + "plugValueWidget:type", "GafferUI.PresetsPlugValueWidget", + + ], + + "selectionMode" : [ + + "description", + """ + Chooses how to select which elements are affected. Only takes effect if you + choose an interpolation other than "Any" or "Constant". "Id List" shows a + list plug to manually select ids. "Id List Primitive Variable" takes + the name of a constant array primitive variable containing a list of ids. + "Mask Primitive Variable" takes the name of a primvar that must match the + selected interpolation - the tweak will apply to all elements where the primitive + variable is non-zero. + """, + + "preset:All", GafferScene.PrimitiveVariableTweaks.SelectionMode.All, + "preset:Id List", GafferScene.PrimitiveVariableTweaks.SelectionMode.IdList, + "preset:Id List Primitive Variable", GafferScene.PrimitiveVariableTweaks.SelectionMode.IdListPrimitiveVariable, + "preset:Mask Primitive Variable", GafferScene.PrimitiveVariableTweaks.SelectionMode.MaskPrimitiveVariable, + + "plugValueWidget:type", "GafferUI.PresetsPlugValueWidget", + + "layout:activator", "selectionModeEnabled", + + ], + + "idList" : [ + + "description", + """ + A list of ids for the elements to affect, corresponding to the current interpolation. For + example, if you choose "Vertex" interpolation, these will be vertex ids. By default, ids + are based on the index, but if you specify an id primitive variable below, the ids in + this list will match the id primitive variable. + """, + + "layout:visibilityActivator", "idListExplicitVisible", + + ], + + "idListVariable" : [ + + "description", + """ + The name of a constant primitive variable containing a list of ids for the elements to affect, + corresponding to the current interpolation. For example, if you choose "Vertex" interpolation, + these will be vertex ids. By default, ids are based on the index, but if you specify an id + primitive variable below, the ids in this list will match the id primitive variable. + """, + + "layout:visibilityActivator", "idListVarVisible", + + ], + + "id" : [ + + "description", + """ + The name of the primitive variable to use as ids. Affects which elements are selected by the idList. + """, + + "layout:visibilityActivator", "idListVisible", + + ], + + "maskVariable" : [ + + "description", + """ + The name of a primitive variable containing a mask. The variable must match the specified interpolation. + Any elements where the mask variable is non-zero will be tweaked. + """, + + "layout:visibilityActivator", "maskVarVisible", + + ], + + "ignoreMissing" : [ + + "description", + """ + Ignores tweaks targeting missing primitive variables. When off, missing primitive variables + cause the node to error. + """, + + ], + + "tweaks" : [ + + "description", + """ + The tweaks to be made to the primitive variables. Arbitrary numbers of user defined + tweaks may be added as children of this plug. + """, + + "plugValueWidget:type", "GafferUI.LayoutPlugValueWidget", + "layout:customWidget:footer:widgetType", "GafferSceneUI.PrimitiveVariableTweaksUI._TweaksFooter", + "layout:customWidget:footer:index", -1, + + "nodule:type", "", + "layout:section", "Settings.Tweaks", + + ], + + "tweaks.*" : [ + + "tweakPlugValueWidget:propertyType", "primitive variable", + + ], + + "tweaks.*.value" : [ + "description", + """ + For a constant primitive variable, this is just the value of the primitive variable. For + non-constant primitive variables, this is the value for each element. + """, + ] + } +) + +########################################################################## +# _TweaksFooter +########################################################################## + +class _TweaksFooter( GafferUI.PlugValueWidget ) : + + def __init__( self, plug ) : + + row = GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal ) + + GafferUI.PlugValueWidget.__init__( self, row, plug ) + + with row : + + GafferUI.Spacer( imath.V2i( GafferUI.PlugWidget.labelWidth(), 1 ) ) + + self.__button = GafferUI.MenuButton( + image = "plus.png", + hasFrame = False, + menu = GafferUI.Menu( Gaffer.WeakMethod( self.__menuDefinition ) ) + ) + + GafferUI.Spacer( imath.V2i( 1 ), imath.V2i( 999999, 1 ), parenting = { "expand" : True } ) + + def _updateFromEditable( self ) : + + # Not using `_editable()` as it considers the whole plug to be non-editable if + # any child has an input connection, but that shouldn't prevent us adding a new + # tweak. + self.__button.setEnabled( self.getPlug().getInput() is None and not Gaffer.MetadataAlgo.readOnly( self.getPlug() ) ) + + def __menuDefinition( self ) : + + result = IECore.MenuDefinition() + + result.append( + "/From Affected", + { + "subMenu" : Gaffer.WeakMethod( self.__addFromAffectedMenuDefinition ) + } + ) + + result.append( + "/From Selection", + { + "subMenu" : Gaffer.WeakMethod( self.__addFromSelectedMenuDefinition ) + } + ) + + result.append( "/FromPathsDivider", { "divider" : True } ) + + for subMenu, items in [ + ( "", [ + Gaffer.BoolPlug, + Gaffer.FloatPlug, + Gaffer.IntPlug, + "NumericDivider", + Gaffer.StringPlug, + "StringDivider", + Gaffer.V2iPlug, + Gaffer.V3iPlug, + Gaffer.V2fPlug, + Gaffer.V3fPlug, + "VectorDivider", + Gaffer.Color3fPlug, + Gaffer.Color4fPlug, + "BoxDivider", + IECore.Box2iData( imath.Box2i( imath.V2i( 0 ), imath.V2i( 1 ) ) ), + IECore.Box2fData( imath.Box2f( imath.V2f( 0 ), imath.V2f( 1 ) ) ), + IECore.Box3iData( imath.Box3i( imath.V3i( 0 ), imath.V3i( 1 ) ) ), + IECore.Box3fData( imath.Box3f( imath.V3f( 0 ), imath.V3f( 1 ) ) ), + "ArrayDivider" + ] ), + ( "Array", [ + IECore.FloatVectorData(), + IECore.IntVectorData(), + IECore.Int64VectorData(), + "StringVectorDivider", + IECore.StringVectorData() + ] ) + ]: + for item in items: + prefix = "/" + subMenu if subMenu else "" + + if isinstance( item, str ) : + result.append( prefix + "/" + item, { "divider" : True } ) + else : + itemName = item.typeName() if isinstance( item, IECore.Data ) else item.__name__ + itemName = itemName.replace( "Plug", "" ).replace( "Data", "" ).replace( "Vector", "" ) + + if hasattr( item, "getInterpretation" ): + itemName += " (" + str( item.getInterpretation() ) + ")" + + result.append( + prefix + "/" + itemName, + { + "command" : functools.partial( Gaffer.WeakMethod( self.__addTweak ), "", item ), + } + ) + + return result + + def __addFromAffectedMenuDefinition( self ) : + + node = self.getPlug().node() + assert( isinstance( node, GafferScene.PrimitiveVariableTweaks ) ) + + pathMatcher = IECore.PathMatcher() + with self.context() : + GafferScene.SceneAlgo.matchingPaths( node["filter"], node["in"], pathMatcher ) + + return self.__addFromPathsMenuDefinition( pathMatcher.paths() ) + + def __addFromSelectedMenuDefinition( self ) : + + return self.__addFromPathsMenuDefinition( + GafferSceneUI.ScriptNodeAlgo.getSelectedPaths( self.scriptNode() ).paths() + ) + + def __addFromPathsMenuDefinition( self, paths ) : + + result = IECore.MenuDefinition() + + node = self.getPlug().node() + assert( isinstance( node, GafferScene.PrimitiveVariableTweaks ) ) + + possibilities = {} + with self.context() : + for path in paths : + obj = node["in"].object( path ) + for name in obj.keys(): + d = obj[name].data + newType = type( d ) + if obj[name].interpolation != IECoreScene.PrimitiveVariable.Interpolation.Constant: + # Convert to element type ( ie V3fVectorData to V3fData ). + # When tweaking non-constant primvars, the tweak must match each element. + newType = IECore.DataTraits.dataTypeFromElementType( IECore.DataTraits.valueTypeFromSequenceType( newType ) ) + newData = newType() + if hasattr( d, "getInterpretation" ): + newData.setInterpretation( d.getInterpretation() ) + + possibilities[name] = newData + + existingTweaks = { tweak["name"].getValue() for tweak in node["tweaks"] } + + possibilities = collections.OrderedDict( sorted( possibilities.items() ) ) + + for key, value in possibilities.items() : + result.append( + "/" + key, + { + "command" : functools.partial( + Gaffer.WeakMethod( self.__addTweak ), + key, + value + ), + "active" : key not in existingTweaks + } + ) + + if not len( result.items() ) : + result.append( + "/No Primitive Variables Found", { "active" : False } + ) + return result + + return result + + def __addTweak( self, privVarName, plugTypeOrValue ) : + + if isinstance( plugTypeOrValue, IECore.Data ) : + plug = Gaffer.TweakPlug( privVarName, plugTypeOrValue ) + else : + plug = Gaffer.TweakPlug( privVarName, plugTypeOrValue() ) + + plug.setName( "tweak0" ) + + with Gaffer.UndoScope( self.getPlug().ancestor( Gaffer.ScriptNode ) ) : + self.getPlug().addChild( plug ) diff --git a/python/GafferSceneUI/__init__.py b/python/GafferSceneUI/__init__.py index c215f034c59..d7361242d5f 100644 --- a/python/GafferSceneUI/__init__.py +++ b/python/GafferSceneUI/__init__.py @@ -204,6 +204,7 @@ from . import MergePointsUI from . import MergeCurvesUI from . import VisualiserToolUI +from . import PrimitiveVariableTweaksUI # then all the PathPreviewWidgets. note that the order # of import controls the order of display. diff --git a/src/GafferScene/PrimitiveVariableTweaks.cpp b/src/GafferScene/PrimitiveVariableTweaks.cpp new file mode 100644 index 00000000000..c2c28f2aed2 --- /dev/null +++ b/src/GafferScene/PrimitiveVariableTweaks.cpp @@ -0,0 +1,575 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2024, Image Engine Design Inc. 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. +// +////////////////////////////////////////////////////////////////////////// + +#include "GafferScene/PrimitiveVariableTweaks.h" + +#include "IECoreScene/Primitive.h" + +#include "IECore/DataAlgo.h" + +#include "IECore/TypeTraits.h" +#include + +using namespace IECore; +using namespace IECoreScene; +using namespace Gaffer; +using namespace GafferScene; + +namespace { + +// Rather startling that this doesn't already exist, but it seems that there isn't anywhere else where we +// report exceptions with interpolations in C++. +std::string interpolationToString( PrimitiveVariable::Interpolation i ) +{ + switch( i ) + { + case PrimitiveVariable::Constant: + return "Constant"; + case PrimitiveVariable::Uniform: + return "Uniform"; + case PrimitiveVariable::Vertex: + return "Vertex"; + case PrimitiveVariable::Varying: + return "Varying"; + case PrimitiveVariable::FaceVarying: + return "FaceVarying"; + default: + return "Invalid"; + }; +} + +} // namespace + +GAFFER_NODE_DEFINE_TYPE( PrimitiveVariableTweaks ); + +size_t PrimitiveVariableTweaks::g_firstPlugIndex = 0; + +PrimitiveVariableTweaks::PrimitiveVariableTweaks( const std::string &name ) + : Deformer( name ) +{ + storeIndexOfNextChild( g_firstPlugIndex ); + + addChild( new IntPlug( "interpolation", Plug::In, PrimitiveVariable::Invalid, PrimitiveVariable::Invalid, PrimitiveVariable::FaceVarying ) ); + addChild( new IntPlug( "selectionMode", Plug::In, (int)SelectionMode::All, (int)SelectionMode::All, (int)SelectionMode::MaskPrimitiveVariable ) ); + addChild( new Int64VectorDataPlug( "idList", Plug::In ) ); + addChild( new StringPlug( "idListVariable", Plug::In, "" ) ); + addChild( new StringPlug( "id", Plug::In, "" ) ); + addChild( new StringPlug( "maskVariable", Plug::In, "" ) ); + addChild( new BoolPlug( "ignoreMissing", Plug::In, false ) ); + addChild( new TweaksPlug( "tweaks" ) ); +} + +PrimitiveVariableTweaks::~PrimitiveVariableTweaks() +{ +} + +Gaffer::IntPlug *PrimitiveVariableTweaks::interpolationPlug() +{ + return getChild( g_firstPlugIndex + 0 ); +} + +const Gaffer::IntPlug *PrimitiveVariableTweaks::interpolationPlug() const +{ + return getChild( g_firstPlugIndex + 0 ); +} + +Gaffer::IntPlug *PrimitiveVariableTweaks::selectionModePlug() +{ + return getChild( g_firstPlugIndex + 1 ); +} + +const Gaffer::IntPlug *PrimitiveVariableTweaks::selectionModePlug() const +{ + return getChild( g_firstPlugIndex + 1 ); +} + +Gaffer::Int64VectorDataPlug *PrimitiveVariableTweaks::idListPlug() +{ + return getChild( g_firstPlugIndex + 2 ); +} + +const Gaffer::Int64VectorDataPlug *PrimitiveVariableTweaks::idListPlug() const +{ + return getChild( g_firstPlugIndex + 2 ); +} + +Gaffer::StringPlug *PrimitiveVariableTweaks::idListVariablePlug() +{ + return getChild( g_firstPlugIndex + 3 ); +} + +const Gaffer::StringPlug *PrimitiveVariableTweaks::idListVariablePlug() const +{ + return getChild( g_firstPlugIndex + 3 ); +} + +Gaffer::StringPlug *PrimitiveVariableTweaks::idPlug() +{ + return getChild( g_firstPlugIndex + 4 ); +} + +const Gaffer::StringPlug *PrimitiveVariableTweaks::idPlug() const +{ + return getChild( g_firstPlugIndex + 4 ); +} + +Gaffer::StringPlug *PrimitiveVariableTweaks::maskVariablePlug() +{ + return getChild( g_firstPlugIndex + 5 ); +} + +const Gaffer::StringPlug *PrimitiveVariableTweaks::maskVariablePlug() const +{ + return getChild( g_firstPlugIndex + 5 ); +} + +Gaffer::BoolPlug *PrimitiveVariableTweaks::ignoreMissingPlug() +{ + return getChild( g_firstPlugIndex + 6 ); +} + +const Gaffer::BoolPlug *PrimitiveVariableTweaks::ignoreMissingPlug() const +{ + return getChild( g_firstPlugIndex + 6 ); +} + +Gaffer::TweaksPlug *PrimitiveVariableTweaks::tweaksPlug() +{ + return getChild( g_firstPlugIndex + 7 ); +} + +const Gaffer::TweaksPlug *PrimitiveVariableTweaks::tweaksPlug() const +{ + return getChild( g_firstPlugIndex + 7 ); +} + +bool PrimitiveVariableTweaks::affectsProcessedObject( const Gaffer::Plug *input ) const +{ + return + Deformer::affectsProcessedObject( input ) || + input == interpolationPlug() || + input == selectionModePlug() || + input == idListPlug() || + input == idListVariablePlug() || + input == idPlug() || + input == maskVariablePlug() || + input == ignoreMissingPlug() || + tweaksPlug()->isAncestorOf( input ) + ; +} + +void PrimitiveVariableTweaks::hashProcessedObject( const ScenePath &path, const Gaffer::Context *context, IECore::MurmurHash &h ) const +{ + if( tweaksPlug()->children().empty() ) + { + h = inPlug()->objectPlug()->hash(); + } + else + { + Deformer::hashProcessedObject( path, context, h ); + interpolationPlug()->hash( h ); + selectionModePlug()->hash( h ); + idListPlug()->hash( h ); + idListVariablePlug()->hash( h ); + idPlug()->hash( h ); + maskVariablePlug()->hash( h ); + ignoreMissingPlug()->hash( h ); + tweaksPlug()->hash( h ); + } +} + +IECore::ConstObjectPtr PrimitiveVariableTweaks::computeProcessedObject( const ScenePath &path, const Gaffer::Context *context, const IECore::Object *inputObject ) const +{ + const Primitive *inputPrimitive = runTimeCast( inputObject ); + if( !inputPrimitive || tweaksPlug()->children().empty() ) + { + return inputObject; + } + + PrimitiveVariable::Interpolation targetInterpolation = (PrimitiveVariable::Interpolation)interpolationPlug()->getValue(); + + PrimitivePtr result = inputPrimitive->copy(); + + SelectionMode selectionMode = (SelectionMode)selectionModePlug()->getValue(); + boost::dynamic_bitset<> mask; + + if( + ( selectionMode == SelectionMode::IdList || selectionMode == SelectionMode::IdListPrimitiveVariable ) && + targetInterpolation != PrimitiveVariable::Invalid && targetInterpolation != PrimitiveVariable::Constant + ) + { + ConstIntVectorDataPtr idList; + ConstInt64VectorDataPtr idList64; + if( selectionMode == SelectionMode::IdList ) + { + idList64 = idListPlug()->getValue(); + } + else + { + std::string idListVarName = idListVariablePlug()->getValue(); + auto idListVar = inputPrimitive->variables.find( idListVarName ); + if( idListVar == inputPrimitive->variables.end() ) + { + throw IECore::Exception( fmt::format( "Can't find id list primitive variable \"{}\".", idListVarName ) ); + + } + + if( idListVar->second.interpolation == PrimitiveVariable::Interpolation::Constant ) + { + if( const Int64VectorData *int64Data = IECore::runTimeCast( idListVar->second.data.get() ) ) + { + idList64 = int64Data; + } + else if( const IntVectorData *intData = IECore::runTimeCast( idListVar->second.data.get() ) ) + { + idList = intData; + } + } + + if( !( idList || idList64 ) ) + { + throw IECore::Exception( fmt::format( "Invalid id list primitive variable \"{}\". A constant IntVector or Int64Vector is required.", idListVarName ) ); + } + + } + + const size_t variableSize = inputPrimitive->variableSize( targetInterpolation ); + mask.resize( variableSize, false ); + std::string idVarName = idPlug()->getValue(); + if( !idVarName.size() ) + { + if( idList64 ) + { + for( int64_t i : idList64->readable() ) + { + if( i >= 0 && i < (int64_t)variableSize ) + { + mask[i] = true; + } + } + } + else + { + for( int i : idList->readable() ) + { + if( i >= 0 && i < (int64_t)variableSize ) + { + mask.set( i, true ); + } + } + } + } + else + { + auto idVar = inputPrimitive->variables.find( idVarName ); + if( idVar == inputPrimitive->variables.end() ) + { + throw IECore::Exception( fmt::format( "Id invalid, can't find primitive variable \"{}\".", idVarName ) ); + } + + if( !inputPrimitive->isPrimitiveVariableValid( idVar->second ) ) + { + throw IECore::Exception( fmt::format( "Id primitive variable \"{}\" is not valid.", idVarName ) ); + } + + if( idVar->second.interpolation != targetInterpolation ) + { + throw IECore::Exception( fmt::format( + "Id variable \"{}\" : Interpolation `{}` doesn't match specified interpolation `{}`.", + idVarName, interpolationToString( idVar->second.interpolation ), interpolationToString( targetInterpolation ) + ) ); + } + + if( idVar->second.indices ) + { + throw IECore::Exception( fmt::format( "Id variable \"{}\" is not allowed to be indexed.", idVarName ) ); + } + + std::unordered_set< int64_t > idSet; + if( idList64 ) + { + for( int64_t i : idList64->readable() ) + { + idSet.insert( i ); + } + } + else + { + for( int i : idList->readable() ) + { + idSet.insert( (int64_t)i ); + } + } + + if( const IntVectorData *intIdsData = IECore::runTimeCast( idVar->second.data.get() ) ) + { + const std::vector &intIds = intIdsData->readable(); + for( size_t i = 0; i < intIds.size(); i++ ) + { + if( idSet.count( intIds[i] ) ) + { + mask.set( i, true ); + } + } + } + else if( const Int64VectorData *int64IdsData = IECore::runTimeCast( idVar->second.data.get() ) ) + { + const std::vector &intIds = int64IdsData->readable(); + for( size_t i = 0; i < intIds.size(); i++ ) + { + if( idSet.count( intIds[i] ) ) + { + mask.set( i, true ); + } + } + } + else + { + throw IECore::Exception( fmt::format( "Id invalid, can't find primitive variable \"{}\" of type IntVectorData or type Int64VectorData.", idVarName ) ); + } + } + } + else if( + selectionMode == SelectionMode::MaskPrimitiveVariable && + targetInterpolation != PrimitiveVariable::Invalid && targetInterpolation != PrimitiveVariable::Constant + ) + { + std::string maskVarName = maskVariablePlug()->getValue(); + auto maskVar = inputPrimitive->variables.find( maskVarName ); + + if( maskVar == inputPrimitive->variables.end() ) + { + throw IECore::Exception( fmt::format( "Can't find mask primitive variable \"{}\".", maskVarName ) ); + } + + if( maskVar->second.interpolation != targetInterpolation ) + { + throw IECore::Exception( fmt::format( + "Mask primitive variable \"{}\" has wrong interpolation `{}`, expected `{}`.", + maskVarName, interpolationToString( maskVar->second.interpolation ), interpolationToString( targetInterpolation ) + ) ); + } + + IECore::dispatch( maskVar->second.data.get(), + [&mask, &maskVar, &maskVarName]( auto *typedData ) + { + using SourceType = typename std::remove_pointer_t; + + // This check should be unnecessary, but IsNumericBasedVectorTypedData fails to compile for + // non-vector types. + if constexpr( TypeTraits::IsVectorTypedData< SourceType >::value ) + { + if constexpr( TypeTraits::IsNumericBasedVectorTypedData< SourceType >::value ) + { + using ValueType = typename SourceType::ValueType::value_type; + + // There a few types that are numeric based, but it doesn't make sense to compare them + // to zero. + if constexpr( !TypeTraits::IsMatrix::value && !TypeTraits::IsQuat::value && !TypeTraits::IsBox::value ) + { + PrimitiveVariable::IndexedView indexedView( maskVar->second ); + ValueType zeroValue( 0 ); + + mask.reserve( indexedView.size() ); + for( size_t i = 0; i < indexedView.size(); i++ ) + { + mask.push_back( indexedView[i] != zeroValue ); + } + return; + } + } + } + + throw IECore::Exception( fmt::format( + "Mask primitive variable \"{}\" has invalid type \"{}\".", + maskVarName, typedData->typeName() + ) ); + } + ); + + } + + for( const auto &tweak : TweakPlug::Range( *tweaksPlug() ) ) + { + if( !tweak->enabledPlug()->getValue() ) + { + continue; + } + + std::string name = tweak->namePlug()->getValue(); + + const TweakPlug::Mode mode = static_cast( tweak->modePlug()->getValue() ); + const TweakPlug::MissingMode missingMode = + ignoreMissingPlug()->getValue() ? TweakPlug::MissingMode::Ignore : TweakPlug::MissingMode::Error; + + auto varIt = result->variables.find( name ); + TweakPlug::DataAndIndices source; + PrimitiveVariable::Interpolation resultInterpolation = targetInterpolation; + if( varIt != result->variables.end() ) + { + if( !result->isPrimitiveVariableValid( varIt->second ) ) + { + throw IECore::Exception( fmt::format( "Cannot tweak \"{}\" : Primitive variable not valid.", name ) ); + } + + source.data = varIt->second.data; + source.indices = varIt->second.indices; + + if( + mode != TweakPlug::Create && mode != TweakPlug::CreateIfMissing && + targetInterpolation != PrimitiveVariable::Invalid && + targetInterpolation != varIt->second.interpolation ) + { + // \todo - Throwing an exception here is probably not the most useful to users. More useful options might + // be "ignore primvars that don't match" or "resample primvars so they do match" ... but we're not sure + // which is right, and we don't want to add additional options to control this unless it's absolutely + // needed. For now, making it an exception makes it easier to modify this behaviour in the future. + // + // Note that one case where the correct behaviour is pretty easy to define is if we are in mode Uniform + // or Vertex, and we encounter a primvar with FaceVarying interpolation. The correct behaviour there is + // is pretty clearly to apply the tweak to all FaceVertices corresponding to the selected Faces or Vertices. + // We haven't implemented this yet, but it would be pretty straightforward to make things behave properly + // instead of throwing in that specific case at least. + throw IECore::Exception( fmt::format( + "Cannot apply tweak to \"{}\" : Interpolation `{}` doesn't match primitive variable interpolation `{}`.", + name, interpolationToString( targetInterpolation ), interpolationToString( varIt->second.interpolation ) + ) ); + } + + // "Create" is the only mode that can change the interpolation of an existing primvar + if( mode != TweakPlug::Create ) + { + resultInterpolation = varIt->second.interpolation; + } + } + + if( resultInterpolation == PrimitiveVariable::Invalid ) + { + // Some of these errors could be handled by TweakPlug, but since we don't know the interpolation to + // use, we don't know whether to call applyTweak or applyElementwiseTweak, so we just deal with + // these errors ourselves. + if( + mode == TweakPlug::Create || mode == TweakPlug::CreateIfMissing || + mode == TweakPlug::ListPrepend || mode == TweakPlug::ListAppend + ) + { + throw IECore::Exception( fmt::format( + "Cannot create primitive variable \"{}\" when interpolation is set to `Any`." + " Please select an interpolation.", name + ) ); + } + else if( + missingMode == Gaffer::TweakPlug::MissingMode::Ignore || + mode == TweakPlug::Remove || mode == Gaffer::TweakPlug::ListRemove + ) + { + continue; + } + else + { + throw IECore::Exception( fmt::format( "Cannot find primitive variable \"{}\" to tweak.", name ) ); + } + + } + else if( resultInterpolation == PrimitiveVariable::Constant ) + { + tweak->applyTweak( + [&source]( const std::string &valueName, const bool withFallback ) + { + return source.data.get(); + }, + [&result, &targetInterpolation]( const std::string &valueName, DataPtr newData ) + { + if( newData ) + { + result->variables[valueName] = PrimitiveVariable( + PrimitiveVariable::Constant, std::move( newData ) + ); + return true; + } + else + { + return result->variables.erase( valueName ) > 0; + } + }, + missingMode + ); + } + else + { + tweak->applyElementwiseTweak( + [&source]( const std::string &valueName, const bool withFallback ) + { + return source; + }, + [&result, &resultInterpolation]( const std::string &valueName, const TweakPlug::DataAndIndices &newPrimVar ) + { + if( !newPrimVar.data ) + { + return result->variables.erase( valueName ) > 0; + } + + result->variables[valueName] = PrimitiveVariable( resultInterpolation, newPrimVar.data, newPrimVar.indices ); + return true; + }, + result->variableSize( resultInterpolation ), + mask.size() ? &mask : nullptr, + missingMode + ); + } + } + + return result; +} + +bool PrimitiveVariableTweaks::adjustBounds() const +{ + if( !Deformer::adjustBounds() ) + { + return false; + } + + + for( const auto &tweak : TweakPlug::Range( *tweaksPlug() ) ) + { + if( tweak->enabledPlug()->getValue() && tweak->namePlug()->getValue() == "P" ) + { + return true; + } + } + + return false; +} diff --git a/src/GafferSceneModule/PrimitiveVariablesBinding.cpp b/src/GafferSceneModule/PrimitiveVariablesBinding.cpp index 4fb8ba3fb87..6bbe63abb48 100644 --- a/src/GafferSceneModule/PrimitiveVariablesBinding.cpp +++ b/src/GafferSceneModule/PrimitiveVariablesBinding.cpp @@ -46,6 +46,7 @@ #include "GafferScene/CollectPrimitiveVariables.h" #include "GafferScene/PrimitiveVariableExists.h" #include "GafferScene/ShufflePrimitiveVariables.h" +#include "GafferScene/PrimitiveVariableTweaks.h" #include "GafferBindings/DependencyNodeBinding.h" @@ -64,4 +65,15 @@ void GafferSceneModule::bindPrimitiveVariables() GafferBindings::DependencyNodeClass(); GafferBindings::DependencyNodeClass(); + { + boost::python::scope tweaksScope = GafferBindings::DependencyNodeClass(); + + boost::python::enum_( "SelectionMode" ) + .value( "All", PrimitiveVariableTweaks::SelectionMode::All ) + .value( "IdList", PrimitiveVariableTweaks::SelectionMode::IdList ) + .value( "IdListPrimitiveVariable", PrimitiveVariableTweaks::SelectionMode::IdListPrimitiveVariable ) + .value( "MaskPrimitiveVariable", PrimitiveVariableTweaks::SelectionMode::MaskPrimitiveVariable ) + ; + } + } diff --git a/startup/gui/menus.py b/startup/gui/menus.py index 260e976bb2c..6f72e491ac2 100644 --- a/startup/gui/menus.py +++ b/startup/gui/menus.py @@ -261,6 +261,7 @@ def __lightCreator( nodeName, shaderName, shape ) : nodeMenu.append( "/Scene/Object/Shuffle Primitive Variables", GafferScene.ShufflePrimitiveVariables, searchText = "ShufflePrimitiveVariables" ) nodeMenu.append( "/Scene/Object/Resample Primitive Variables", GafferScene.ResamplePrimitiveVariables, searchText = "ResamplePrimitiveVariables" ) nodeMenu.append( "/Scene/Object/Collect Primitive Variables", GafferScene.CollectPrimitiveVariables, searchText = "CollectPrimitiveVariables" ) +nodeMenu.append( "/Scene/Object/Primitive Variable Tweaks", GafferScene.PrimitiveVariableTweaks, searchText = "PrimitiveVariableTweaks" ) nodeMenu.append( "/Scene/Object/Orientation", GafferScene.Orientation ) nodeMenu.append( "/Scene/Object/Mesh Type", GafferScene.MeshType, searchText = "MeshType" ) nodeMenu.append( "/Scene/Object/Points Type", GafferScene.PointsType, searchText = "PointsType" )