From eed1e18f5740111d724223c1eda28879ccb2114f Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Thu, 8 Feb 2024 15:20:56 -0800 Subject: [PATCH 1/9] adding support for array parameters as vectordata --- include/Gaffer/PlugAlgo.h | 4 ++ src/Gaffer/PlugAlgo.cpp | 61 +++++++++++++++++++++++++++ src/GafferArnold/ParameterHandler.cpp | 25 +++++++++++ src/GafferScene/Shader.cpp | 20 ++++++--- 4 files changed, 104 insertions(+), 6 deletions(-) diff --git a/include/Gaffer/PlugAlgo.h b/include/Gaffer/PlugAlgo.h index d69d292aef9..9102a7056e8 100644 --- a/include/Gaffer/PlugAlgo.h +++ b/include/Gaffer/PlugAlgo.h @@ -50,6 +50,7 @@ namespace Gaffer IE_CORE_FORWARDDECLARE( GraphComponent ) IE_CORE_FORWARDDECLARE( ValuePlug ) +IE_CORE_FORWARDDECLARE( ArrayPlug ) namespace PlugAlgo { @@ -86,6 +87,9 @@ GAFFER_API ValuePlugPtr createPlugFromData( const std::string &name, Plug::Direc /// Returns a Data value from a plug. GAFFER_API IECore::DataPtr getValueAsData( const ValuePlug *plug ); +/// Returns a VectorData value from an array plug. +GAFFER_API IECore::DataPtr getArrayAsVectorData( const ArrayPlug *plug ); + /// Sets the value of an existing plug to the specified data. /// Returns `true` on success and `false` on failure. GAFFER_API bool setValueFromData( ValuePlug *plug, const IECore::Data *value ); diff --git a/src/Gaffer/PlugAlgo.cpp b/src/Gaffer/PlugAlgo.cpp index 1d0bf6424fd..7c6ab968f72 100644 --- a/src/Gaffer/PlugAlgo.cpp +++ b/src/Gaffer/PlugAlgo.cpp @@ -48,6 +48,7 @@ #include "Gaffer/TransformPlug.h" #include "Gaffer/TypedObjectPlug.h" #include "Gaffer/ValuePlug.h" +#include "Gaffer/ArrayPlug.h" #include "IECore/SplineData.h" @@ -532,6 +533,66 @@ IECore::DataPtr getValueAsData( const ValuePlug *plug ) } +IECore::DataPtr getArrayAsVectorData( const ArrayPlug *plug ) +{ + size_t size = plug->children().size(); + if ( !size ) + { + return nullptr; + } + auto *childPlug = plug->getChild( 0 ); + int i = 0; + switch(static_cast(childPlug->typeId())) + { + case FloatPlugTypeId : + { + FloatVectorDataPtr data = new FloatVectorData(); + data->writable().resize( size ); + for( Plug::InputIterator it( plug ); !it.done(); ++it, ++i ) + { + data->writable()[i] = static_cast( it->get() )->getValue(); + } + return data; + } + case IntPlugTypeId : + { + IntVectorDataPtr data = new IntVectorData(); + data->writable().resize( size ); + for( Plug::InputIterator it( plug ); !it.done(); ++it, ++i ) + { + data->writable()[i] = static_cast( it->get() )->getValue(); + } + return data; + } + case Color3fPlugTypeId : + { + Color3fVectorDataPtr data = new Color3fVectorData(); + data->writable().resize( size ); + for( Plug::InputIterator it( plug ); !it.done(); ++it, ++i ) + { + data->writable()[i] = static_cast( it->get() )->getValue(); + } + return data; + } + case Color4fPlugTypeId : + { + Color4fVectorDataPtr data = new Color4fVectorData(); + data->writable().resize( size ); + for( Plug::InputIterator it( plug ); !it.done(); ++it, ++i ) + { + data->writable()[i] = static_cast( it->get() )->getValue(); + } + return data; + } + default : + throw IECore::Exception( + fmt::format( "Plug \"{}\" has unsupported type \"{}\"", childPlug->getName().string(), childPlug->typeName() ) + ); + } + +} + + IECore::DataPtr extractDataFromPlug( const ValuePlug *plug ) { return getValueAsData( plug ); diff --git a/src/GafferArnold/ParameterHandler.cpp b/src/GafferArnold/ParameterHandler.cpp index 5119d65f5bd..36e0dc690a9 100644 --- a/src/GafferArnold/ParameterHandler.cpp +++ b/src/GafferArnold/ParameterHandler.cpp @@ -45,6 +45,7 @@ #include "Gaffer/ScriptNode.h" #include "Gaffer/StringPlug.h" #include "Gaffer/TypedPlug.h" +#include "Gaffer/ArrayPlug.h" #include "IECore/MessageHandler.h" @@ -146,6 +147,26 @@ Gaffer::Plug *setupNumericPlug( const AtNodeEntry *node, const AtParamEntry *par return plug.get(); } +template +Gaffer::Plug *setupArrayPlug( const IECore::InternedString ¶meterName, Gaffer::GraphComponent *plugParent, Gaffer::Plug::Direction direction ) +{ + PlugType *existingPlug = plugParent->getChild( parameterName ); + + if( + existingPlug && + existingPlug->direction() == direction + ) + { + existingPlug->setFlags( Gaffer::Plug::Dynamic, false ); + return existingPlug; + } + + typename PlugType::Ptr plug = new PlugType( parameterName, direction, nullptr, 1, std::numeric_limits::max(), Plug::Flags::Default, false ); + PlugAlgo::replacePlug( plugParent, plug ); + + return plug.get(); +} + Gaffer::Plug *setupPlug( const IECore::InternedString ¶meterName, Gaffer::GraphComponent *plugParent, Gaffer::Plug::Direction direction ) { Plug *existingPlug = plugParent->getChild( parameterName ); @@ -572,6 +593,10 @@ Gaffer::Plug *ParameterHandler::setupPlug( const AtNodeEntry *node, const AtPara ); break; + case AI_TYPE_ARRAY : + plug = setupArrayPlug( AiParamGetName( parameter ).c_str(), plugParent, direction); + break; + } if( !plug ) diff --git a/src/GafferScene/Shader.cpp b/src/GafferScene/Shader.cpp index b3e0839e5f2..d26a3423dc7 100644 --- a/src/GafferScene/Shader.cpp +++ b/src/GafferScene/Shader.cpp @@ -430,12 +430,7 @@ class Shader::NetworkBuilder } else if( const Gaffer::ArrayPlug *array = IECore::runTimeCast( parameter ) ) { - int i = 0; - for( Plug::InputIterator it( array ); !it.done(); ++it, ++i ) - { - IECore::InternedString childParameterName = parameterName.string() + "[" + std::to_string( i ) + "]"; - addParameter( it->get(), childParameterName, shader, connections ); - } + addParameter( parameter, parameterName, shader, connections ); } else { @@ -617,6 +612,15 @@ class Shader::NetworkBuilder { addSplineParameterComponentConnections< SplinefColor4fPlug >( (const SplinefColor4fPlug*)parameter, parameterName, connections ); } + else if ( (Gaffer::TypeId)parameter->typeId() == ArrayPlugTypeId ) + { + int i = 0; + for ( Plug::InputIterator it( parameter ); !it.done(); ++it, ++i ) + { + IECore::InternedString inputName = parameterName.string() + "[" + std::to_string( i ) + "]"; + addParameterComponentConnections( it->get(), inputName, connections ); + } + } } template< typename T > @@ -1059,6 +1063,10 @@ IECore::DataPtr Shader::parameterValue( const Gaffer::Plug *parameterPlug ) cons { return Gaffer::PlugAlgo::getValueAsData( valuePlug ); } + else if( auto arrayPlug = IECore::runTimeCast( parameterPlug ) ) + { + return Gaffer::PlugAlgo::getArrayAsVectorData( arrayPlug ); + } return nullptr; } From eb76316bd5effbeb28da38507bcd81dd9d385f89 Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Wed, 28 Feb 2024 11:59:14 -0800 Subject: [PATCH 2/9] adding support for connections of other data types --- src/GafferScene/Shader.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/GafferScene/Shader.cpp b/src/GafferScene/Shader.cpp index d26a3423dc7..0fb8f1b4931 100644 --- a/src/GafferScene/Shader.cpp +++ b/src/GafferScene/Shader.cpp @@ -621,6 +621,19 @@ class Shader::NetworkBuilder addParameterComponentConnections( it->get(), inputName, connections ); } } + else + { + const Gaffer::Plug *effectiveParameter = this->effectiveParameter( parameter ); + if( effectiveParameter && isOutputParameter( effectiveParameter ) ) + { + IECore::InternedString inputName = parameterName.string(); + + connections.push_back( { + outputParameterForPlug( effectiveParameter ), + { IECore::InternedString(), inputName } + } ); + } + } } template< typename T > From 9634b89a26c3a189973673c9a843fb84df512e4a Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Wed, 22 May 2024 16:15:51 -0700 Subject: [PATCH 3/9] adding remaining array types --- src/Gaffer/PlugAlgo.cpp | 52 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/Gaffer/PlugAlgo.cpp b/src/Gaffer/PlugAlgo.cpp index 7c6ab968f72..6b35113f0e0 100644 --- a/src/Gaffer/PlugAlgo.cpp +++ b/src/Gaffer/PlugAlgo.cpp @@ -584,9 +584,59 @@ IECore::DataPtr getArrayAsVectorData( const ArrayPlug *plug ) } return data; } + case StringPlugTypeId : + { + StringVectorDataPtr data = new StringVectorData(); + data->writable().resize( size ); + for( Plug::InputIterator it( plug ); !it.done(); ++it, ++i ) + { + data->writable()[i] = static_cast( it->get() )->getValue(); + } + return data; + } + case BoolPlugTypeId : + { + BoolVectorDataPtr data = new BoolVectorData(); + data->writable().resize( size ); + for( Plug::InputIterator it( plug ); !it.done(); ++it, ++i ) + { + data->writable()[i] = static_cast( it->get() )->getValue(); + } + return data; + } + case V2fPlugTypeId : + { + V2fVectorDataPtr data = new V2fVectorData(); + data->writable().resize( size ); + for( Plug::InputIterator it( plug ); !it.done(); ++it, ++i ) + { + data->writable()[i] = static_cast( it->get() )->getValue(); + } + return data; + } + case V3fPlugTypeId : + { + V3fVectorDataPtr data = new V3fVectorData(); + data->writable().resize( size ); + for( Plug::InputIterator it( plug ); !it.done(); ++it, ++i ) + { + data->writable()[i] = static_cast( it->get() )->getValue(); + } + return data; + } + case M44fPlugTypeId : + { + M44fVectorDataPtr data = new M44fVectorData(); + data->writable().resize( size ); + for( Plug::InputIterator it( plug ); !it.done(); ++it, ++i ) + { + data->writable()[i] = static_cast( it->get() )->getValue(); + } + return data; + } default : throw IECore::Exception( - fmt::format( "Plug \"{}\" has unsupported type \"{}\"", childPlug->getName().string(), childPlug->typeName() ) + fmt::format( "ArrayPlug \"{}\" has unsupported child type \"{}\"", plug->getName().string(), childPlug->typeName() ) ); } From c902277e555b1e3ab2f2badd268fb687ab45442b Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Wed, 2 Oct 2024 19:54:33 -0700 Subject: [PATCH 4/9] adding type to created arnold array --- src/GafferArnold/ParameterHandler.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/GafferArnold/ParameterHandler.cpp b/src/GafferArnold/ParameterHandler.cpp index 066b45ff8f1..7628979acbe 100644 --- a/src/GafferArnold/ParameterHandler.cpp +++ b/src/GafferArnold/ParameterHandler.cpp @@ -152,10 +152,9 @@ Gaffer::Plug *setupNumericPlug( const AtNodeEntry *node, const AtParamEntry *par return plug.get(); } -template -Gaffer::Plug *setupArrayPlug( const IECore::InternedString ¶meterName, Gaffer::GraphComponent *plugParent, Gaffer::Plug::Direction direction ) +Gaffer::Plug *setupArrayPlug( const IECore::InternedString ¶meterName, Gaffer::GraphComponent *plugParent, Gaffer::Plug::Direction direction, int arrayType ) { - PlugType *existingPlug = plugParent->getChild( parameterName ); + Plug *existingPlug = plugParent->getChild( parameterName ); if( existingPlug && @@ -165,8 +164,8 @@ Gaffer::Plug *setupArrayPlug( const IECore::InternedString ¶meterName, Gaffe existingPlug->setFlags( Gaffer::Plug::Dynamic, false ); return existingPlug; } - - typename PlugType::Ptr plug = new PlugType( parameterName, direction, nullptr, 1, std::numeric_limits::max(), Plug::Flags::Default, false ); + Plug *arrayTypePlug = ParameterHandler::setupPlug(parameterName, arrayType, plugParent, direction ); + ArrayPlugPtr plug = new ArrayPlug( parameterName, direction, arrayTypePlug, 0, std::numeric_limits::max(), Plug::Flags::Default, false ); PlugAlgo::replacePlug( plugParent, plug ); return plug.get(); @@ -599,7 +598,8 @@ Gaffer::Plug *ParameterHandler::setupPlug( const AtNodeEntry *node, const AtPara break; case AI_TYPE_ARRAY : - plug = setupArrayPlug( AiParamGetName( parameter ).c_str(), plugParent, direction); + int arrayType = AiParamGetSubType(parameter); + plug = setupArrayPlug( AiParamGetName( parameter ).c_str(), plugParent, direction, arrayType); break; } From 77420aab8bcc1b041b55c7b36ba40934f421c5a5 Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Wed, 2 Oct 2024 20:14:18 -0700 Subject: [PATCH 5/9] minimizing calls to writable for efficiency --- src/Gaffer/PlugAlgo.cpp | 45 ++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/Gaffer/PlugAlgo.cpp b/src/Gaffer/PlugAlgo.cpp index e54583e1f18..f8ea4353291 100644 --- a/src/Gaffer/PlugAlgo.cpp +++ b/src/Gaffer/PlugAlgo.cpp @@ -564,90 +564,99 @@ IECore::DataPtr getArrayAsVectorData( const ArrayPlug *plug ) case FloatPlugTypeId : { FloatVectorDataPtr data = new FloatVectorData(); - data->writable().resize( size ); + auto &writable = data->writable(); + writable.resize( size ); for( Plug::InputIterator it( plug ); !it.done(); ++it, ++i ) { - data->writable()[i] = static_cast( it->get() )->getValue(); + writable[i] = static_cast( it->get() )->getValue(); } return data; } case IntPlugTypeId : { IntVectorDataPtr data = new IntVectorData(); - data->writable().resize( size ); + auto &writable = data->writable(); + writable.resize( size ); for( Plug::InputIterator it( plug ); !it.done(); ++it, ++i ) { - data->writable()[i] = static_cast( it->get() )->getValue(); + writable[i] = static_cast( it->get() )->getValue(); } return data; } case Color3fPlugTypeId : { Color3fVectorDataPtr data = new Color3fVectorData(); - data->writable().resize( size ); + auto &writable = data->writable(); + writable.resize( size ); for( Plug::InputIterator it( plug ); !it.done(); ++it, ++i ) { - data->writable()[i] = static_cast( it->get() )->getValue(); + writable[i] = static_cast( it->get() )->getValue(); } return data; } case Color4fPlugTypeId : { Color4fVectorDataPtr data = new Color4fVectorData(); - data->writable().resize( size ); + auto &writable = data->writable(); + writable.resize( size ); for( Plug::InputIterator it( plug ); !it.done(); ++it, ++i ) { - data->writable()[i] = static_cast( it->get() )->getValue(); + writable[i] = static_cast( it->get() )->getValue(); } return data; } case StringPlugTypeId : { StringVectorDataPtr data = new StringVectorData(); - data->writable().resize( size ); + auto &writable = data->writable(); + writable.resize( size ); for( Plug::InputIterator it( plug ); !it.done(); ++it, ++i ) { - data->writable()[i] = static_cast( it->get() )->getValue(); + writable[i] = static_cast( it->get() )->getValue(); } return data; } case BoolPlugTypeId : { BoolVectorDataPtr data = new BoolVectorData(); - data->writable().resize( size ); + auto &writable = data->writable(); + writable.resize( size ); for( Plug::InputIterator it( plug ); !it.done(); ++it, ++i ) { - data->writable()[i] = static_cast( it->get() )->getValue(); + writable[i] = static_cast( it->get() )->getValue(); } return data; } case V2fPlugTypeId : { V2fVectorDataPtr data = new V2fVectorData(); - data->writable().resize( size ); + auto &writable = data->writable(); + writable.resize( size ); for( Plug::InputIterator it( plug ); !it.done(); ++it, ++i ) { - data->writable()[i] = static_cast( it->get() )->getValue(); + writable[i] = static_cast( it->get() )->getValue(); } return data; } case V3fPlugTypeId : { V3fVectorDataPtr data = new V3fVectorData(); - data->writable().resize( size ); + auto &writable = data->writable(); + writable.resize( size ); for( Plug::InputIterator it( plug ); !it.done(); ++it, ++i ) { - data->writable()[i] = static_cast( it->get() )->getValue(); + writable[i] = static_cast( it->get() )->getValue(); } return data; } case M44fPlugTypeId : { M44fVectorDataPtr data = new M44fVectorData(); - data->writable().resize( size ); + auto &writable = data->writable(); + writable.resize( size ); for( Plug::InputIterator it( plug ); !it.done(); ++it, ++i ) { - data->writable()[i] = static_cast( it->get() )->getValue(); + writable[i] = static_cast( it->get() )->getValue(); } return data; } From 48cec0d40de0f16b9975d9792d6091d28aa8c829 Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Wed, 5 Feb 2025 18:26:52 -0800 Subject: [PATCH 6/9] adding ContextProcessor support --- src/GafferScene/Shader.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/GafferScene/Shader.cpp b/src/GafferScene/Shader.cpp index 008137ab183..6004253c69b 100644 --- a/src/GafferScene/Shader.cpp +++ b/src/GafferScene/Shader.cpp @@ -694,7 +694,8 @@ class Shader::NetworkBuilder } else { - const Gaffer::Plug *effectiveParameter = this->effectiveParameter( parameter ); + OptionalScopedContext parameterContext; + const Gaffer::Plug *effectiveParameter = this->effectiveParameter( parameter, parameterContext ); if( effectiveParameter && isOutputParameter( effectiveParameter ) ) { IECore::InternedString inputName = parameterName.string(); From fa13f760dc4e525ca193481ad262f78bb5fe37a7 Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Mon, 24 Aug 2026 12:28:24 -0700 Subject: [PATCH 7/9] hashing support for array params --- src/GafferScene/Shader.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/GafferScene/Shader.cpp b/src/GafferScene/Shader.cpp index 6004253c69b..88c45831cec 100644 --- a/src/GafferScene/Shader.cpp +++ b/src/GafferScene/Shader.cpp @@ -604,6 +604,24 @@ class Shader::NetworkBuilder { hashSplineParameterComponentConnections< SplinefColor4fPlug >( (const SplinefColor4fPlug*)parameter, h ); } + else if ( (Gaffer::TypeId)parameter->typeId() == ArrayPlugTypeId ) + { + int i = 0; + for ( Plug::InputIterator it( parameter ); !it.done(); ++it, ++i ) + { + hashParameterComponentConnections( it->get(), h ); + } + } + else + { + OptionalScopedContext parameterContext; + const Gaffer::Plug *effectiveParameter = this->effectiveParameter( parameter, parameterContext ); + if( effectiveParameter && isOutputParameter( effectiveParameter ) ) + { + parameterHashForPlug( effectiveParameter, h ); + h.append( parameter->getName() ); + } + } } template< typename T > @@ -694,6 +712,7 @@ class Shader::NetworkBuilder } else { + // handling of array children that do not have components OptionalScopedContext parameterContext; const Gaffer::Plug *effectiveParameter = this->effectiveParameter( parameter, parameterContext ); if( effectiveParameter && isOutputParameter( effectiveParameter ) ) From 4abbad2fc0f2e5312c2d809b982283abbbc2d0c5 Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Mon, 24 Aug 2026 12:29:26 -0700 Subject: [PATCH 8/9] Array parameter node UI based on VectorData UI --- python/GafferUI/ArrayDataWidget.py | 104 ++++++++++++++++++++++++ python/GafferUI/ArrayPlugValueWidget.py | 97 ++++++++++++++++++++++ python/GafferUI/__init__.py | 2 + 3 files changed, 203 insertions(+) create mode 100644 python/GafferUI/ArrayDataWidget.py create mode 100644 python/GafferUI/ArrayPlugValueWidget.py diff --git a/python/GafferUI/ArrayDataWidget.py b/python/GafferUI/ArrayDataWidget.py new file mode 100644 index 00000000000..2530ef4f14f --- /dev/null +++ b/python/GafferUI/ArrayDataWidget.py @@ -0,0 +1,104 @@ +import Gaffer +import GafferUI +from GafferUI.VectorDataWidget import _Model as _VectorDataWidgetModel +from GafferUI.VectorDataWidget import _Delegate as _VectorDataWidgetDelegate + +from Qt import QtCore +from Qt import QtWidgets +from Qt import QtCompat + + +class ArrayDataWidget( GafferUI.VectorDataWidget ) : + def __init__( self, data=None, **kwargs ) : + self.__rowEditability = None + super( ArrayDataWidget, self ).__init__( data, **kwargs ) + + + def setData( self, data ) : + + if data is not None : + if not isinstance( data, list ) : + data = [ data ] + self._VectorDataWidget__model = _Model( data, self._VectorDataWidget__tableView, self.getEditable(), self._VectorDataWidget__header, self._VectorDataWidget__toolTips, self._VectorDataWidget__columnEditability, self.__rowEditability ) + self._VectorDataWidget__model.dataChanged.connect( Gaffer.WeakMethod( self._VectorDataWidget__modelDataChanged ) ) + self._VectorDataWidget__model.rowsInserted.connect( Gaffer.WeakMethod( self._VectorDataWidget__emitDataChangedSignal ) ) + self._VectorDataWidget__model.rowsRemoved.connect( Gaffer.WeakMethod( self._VectorDataWidget__emitDataChangedSignal ) ) + else : + self._VectorDataWidget__model = None + + self._VectorDataWidget__tableView.setModel( self._VectorDataWidget__model ) + + if self._VectorDataWidget__model : + + columnIndex = 0 + haveResizeableContents = False + for accessor in self._VectorDataWidget__model.vectorDataAccessors() : + for i in range( 0, accessor.numColumns() ) : + delegate = _VectorDataWidgetDelegate.create( accessor.data() ) + delegate.setParent( self._VectorDataWidget__model ) + self._VectorDataWidget__tableView.setItemDelegateForColumn( columnIndex, delegate ) + canStretch = delegate.canStretch() + haveResizeableContents = haveResizeableContents or canStretch + columnIndex += 1 + + QtCompat.setSectionResizeMode( + self._VectorDataWidget__tableView.horizontalHeader(), + QtWidgets.QHeaderView.ResizeToContents if haveResizeableContents else QtWidgets.QHeaderView.Fixed + ) + + self._VectorDataWidget__tableView.horizontalHeader().setStretchLastSection( canStretch ) + horizontalSizePolicy = QtWidgets.QSizePolicy.Expanding + if self._VectorDataWidget__tableView.horizontalScrollMode() == QtCore.Qt.ScrollBarAlwaysOff and not canStretch : + horizontalSizePolicy = QtWidgets.QSizePolicy.Fixed + + self._VectorDataWidget__tableView.setSizePolicy( + QtWidgets.QSizePolicy( + horizontalSizePolicy, + QtWidgets.QSizePolicy.Maximum + ) + ) + + selectionModel = self._VectorDataWidget__tableView.selectionModel() + selectionModel.selectionChanged.connect( Gaffer.WeakMethod( self._VectorDataWidget__selectionChanged ) ) + + self._VectorDataWidget__updateRemoveButtonEnabled() + + self._VectorDataWidget__tableView.verticalHeader().setUpdatesEnabled( True ) + self._VectorDataWidget__tableView.updateGeometry() + + def setRowEditability( self, rowEditability ) : + + self.__rowEditability = rowEditability + + def getRowEditability( self ) : + + return self.__rowEditability + + +class _Model( _VectorDataWidgetModel ) : + + def __init__( self, data, parent=None, editable=True, header=None, toolTips=None, columnEditability=None, rowEditability=None ) : + _VectorDataWidgetModel.__init__( self, data, parent=parent, editable=editable, header=header, toolTips=toolTips, columnEditability=columnEditability ) + + self.__rowEditability = rowEditability + + def flags( self, index ) : + result = ( + QtCore.Qt.ItemIsSelectable | + QtCore.Qt.ItemIsDragEnabled + ) + + if self.__editable : + rowEditable = ( + self.__rowEditability is None or + index.row() >= len( self.__rowEditability ) or + self.__rowEditability[index.row()] + ) + if rowEditable : + result |= QtCore.Qt.ItemIsEnabled + if self.__columnEditability is None or self.__columnEditability[index.column()] : + result |= QtCore.Qt.ItemIsEditable + else : + result |= QtCore.Qt.ItemIsEnabled + + return result \ No newline at end of file diff --git a/python/GafferUI/ArrayPlugValueWidget.py b/python/GafferUI/ArrayPlugValueWidget.py new file mode 100644 index 00000000000..9baabfbae54 --- /dev/null +++ b/python/GafferUI/ArrayPlugValueWidget.py @@ -0,0 +1,97 @@ +import IECore + +import Gaffer +import GafferUI + + +class ArrayPlugValueWidget( GafferUI.PlugValueWidget ) : + + def __init__( self, plug, **kw ) : + + sizeEditable = plug.minSize() != plug.maxSize() + + self.__dataWidget = GafferUI.ArrayDataWidget( + header = True, + sizeEditable = sizeEditable, + ) + + GafferUI.PlugValueWidget.__init__( self, self.__dataWidget, plug, **kw ) + + self.__dataWidget.dataChangedSignal().connect( Gaffer.WeakMethod( self.__dataChanged ) ) + + def setHighlighted( self, highlighted ) : + + GafferUI.PlugValueWidget.setHighlighted( self, highlighted ) + self.__dataWidget.setHighlighted( highlighted ) + + + @staticmethod + def _valuesForUpdate( plugs, auxiliaryPlugs ) : + + assert( len( plugs ) == 1 ) + plug = next( iter( plugs ) ) + children = list( plug ) + return { + "values" : [ c.getValue() for c in children ], + "rowEditable" : [ c.getInput() is None for c in children ], + } + + def _updateFromValues( self, values, exception ) : + + if not values : + return + + vectorDataType = None + + plug = self.getPlug() + if plug is not None : + vectorDataType = self.__vectorDataType( plug ) + + if vectorDataType is not None : + self.__dataWidget.setRowEditability( values["rowEditable"] ) + self.__dataWidget.setData( vectorDataType( values["values"] ) ) + self.__dataWidget.setErrored( exception is not None ) + + def _updateFromEditable( self ) : + + self.__dataWidget.setEditable( self._editable() ) + + def __dataChanged( self, widget ) : + + plug = self.getPlug() + if plug is None : + return + + with self._blockedUpdateFromValues() : + with Gaffer.UndoScope( plug.ancestor( Gaffer.ScriptNode ) ) : + data = self.__dataWidget.getData()[0] + targetSize = max( plug.minSize(), min( plug.maxSize(), len( data ) ) ) + plug.resize( targetSize ) + for i, child in enumerate( plug ) : + if i < len( data ) and child.getInput() is None : + child.setValue( data[i] ) + + self._requestUpdateFromValues() + + @staticmethod + def __vectorDataType( plug ) : + + elementPrototype = plug.elementPrototype() + if elementPrototype is None : + return None + return ArrayPlugValueWidget.__plugTypeToVectorDataType.get( type( elementPrototype ) ) + + __plugTypeToVectorDataType = { + Gaffer.BoolPlug : IECore.BoolVectorData, + Gaffer.IntPlug : IECore.IntVectorData, + Gaffer.FloatPlug : IECore.FloatVectorData, + Gaffer.StringPlug : IECore.StringVectorData, + Gaffer.V2iPlug : IECore.V2iVectorData, + Gaffer.V2fPlug : IECore.V2fVectorData, + Gaffer.V3iPlug : IECore.V3iVectorData, + Gaffer.V3fPlug : IECore.V3fVectorData, + Gaffer.Color3fPlug : IECore.Color3fVectorData, + Gaffer.Color4fPlug : IECore.Color4fVectorData, + } + +GafferUI.PlugValueWidget.registerType( Gaffer.ArrayPlug, ArrayPlugValueWidget ) diff --git a/python/GafferUI/__init__.py b/python/GafferUI/__init__.py index c2bd177c343..72a39bfdfc6 100644 --- a/python/GafferUI/__init__.py +++ b/python/GafferUI/__init__.py @@ -161,6 +161,7 @@ def __shiboken() : from .ErrorDialogue import ErrorDialogue from ._Variant import _Variant from .VectorDataWidget import VectorDataWidget +from .ArrayDataWidget import ArrayDataWidget from .PathVectorDataWidget import PathVectorDataWidget from .ProgressBar import ProgressBar from .SelectionMenu import SelectionMenu @@ -205,6 +206,7 @@ def __shiboken() : from .PathPlugValueWidget import PathPlugValueWidget from .FileSystemPathPlugValueWidget import FileSystemPathPlugValueWidget from .VectorDataPlugValueWidget import VectorDataPlugValueWidget +from .ArrayPlugValueWidget import ArrayPlugValueWidget from .PathVectorDataPlugValueWidget import PathVectorDataPlugValueWidget from .FileSystemPathVectorDataPlugValueWidget import FileSystemPathVectorDataPlugValueWidget from .PlugWidget import PlugWidget From d90deda30abfadca51c74f163cdedf4f19923f16 Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Mon, 24 Aug 2026 12:29:51 -0700 Subject: [PATCH 9/9] Array parameter tests --- python/GafferArnoldTest/ArnoldShaderTest.py | 24 +++++ python/GafferUITest/ArrayDataWidgetTest.py | 104 ++++++++++++++++++++ python/GafferUITest/__init__.py | 1 + 3 files changed, 129 insertions(+) create mode 100644 python/GafferUITest/ArrayDataWidgetTest.py diff --git a/python/GafferArnoldTest/ArnoldShaderTest.py b/python/GafferArnoldTest/ArnoldShaderTest.py index d344e5682c1..27634b2d1d3 100644 --- a/python/GafferArnoldTest/ArnoldShaderTest.py +++ b/python/GafferArnoldTest/ArnoldShaderTest.py @@ -989,5 +989,29 @@ def testUSDRoundTrip( self ) : ignoreBlindData = True ) + def testArrayConnections ( self ): + + ramp = GafferArnold.ArnoldShader("ramp") + ramp.loadShader( "ramp_rgb" ) + ramp['parameters']['position'].resize(2) + ramp['parameters']['color'].resize(2) + ramp['parameters']['interpolation'].resize(2) + + userDataRgb = GafferArnold.ArnoldShader() + userDataRgb.loadShader("user_data_rgb") + + userDataFloat = GafferArnold.ArnoldShader() + userDataFloat.loadShader( "user_data_float" ) + + ramp['parameters']['position'][1].setInput(userDataFloat['out']) + ramp['parameters']['color'][1].setInput(userDataRgb['out']) + + n = ramp.attributes() + + self.assertTrue( ramp["parameters"]["color"][1].acceptsInput( userDataRgb["out"] ) ) + self.assertTrue( ramp["parameters"]["position"][1].acceptsInput( userDataFloat["out"] ) ) + # 3 RGB connections and 1 float connection + self.assertTrue(len(n["ai:surface"].inputConnections("ramp")) == 4) + if __name__ == "__main__": unittest.main() diff --git a/python/GafferUITest/ArrayDataWidgetTest.py b/python/GafferUITest/ArrayDataWidgetTest.py new file mode 100644 index 00000000000..88b8e97f118 --- /dev/null +++ b/python/GafferUITest/ArrayDataWidgetTest.py @@ -0,0 +1,104 @@ +import unittest +import imath + +import IECore + +import GafferTest +import GafferUI +import GafferUITest +from Qt import QtCore + +class ArrayDataWidgetTest( GafferUITest.TestCase ) : + + def testIndexing( self ) : + + data = [ + IECore.FloatVectorData( range( 0, 3 ) ), + IECore.Color3fVectorData( [ imath.Color3f( x ) for x in range( 0, 3 ) ] ), + IECore.StringVectorData( [ str( x ) for x in range( 0, 3 ) ] ), + IECore.IntVectorData( range( 0, 3 ) ), + IECore.V3fVectorData( [ imath.V3f( x ) for x in range( 0, 3 ) ] ), + ] + + w = GafferUI.ArrayDataWidget( data ) + + self.assertEqual( w.columnToDataIndex( 0 ), ( 0, -1 ) ) + self.assertEqual( w.columnToDataIndex( 1 ), ( 1, 0 ) ) + self.assertEqual( w.columnToDataIndex( 2 ), ( 1, 1 ) ) + self.assertEqual( w.columnToDataIndex( 3 ), ( 1, 2 ) ) + self.assertEqual( w.columnToDataIndex( 4 ), ( 1, 3 ) ) + self.assertEqual( w.columnToDataIndex( 5 ), ( 2, -1 ) ) + self.assertEqual( w.columnToDataIndex( 6 ), ( 3, -1 ) ) + self.assertEqual( w.columnToDataIndex( 7 ), ( 4, 0 ) ) + self.assertEqual( w.columnToDataIndex( 8 ), ( 4, 1 ) ) + self.assertEqual( w.columnToDataIndex( 9 ), ( 4, 2 ) ) + + self.assertRaises( IndexError, w.columnToDataIndex, 10 ) + + self.assertEqual( w.dataToColumnIndex( 0, -1 ), 0 ) + self.assertEqual( w.dataToColumnIndex( 1, 0 ), 1 ) + self.assertEqual( w.dataToColumnIndex( 1, 1 ), 2 ) + self.assertEqual( w.dataToColumnIndex( 1, 2 ), 3 ) + self.assertEqual( w.dataToColumnIndex( 1, 3 ), 4 ) + self.assertEqual( w.dataToColumnIndex( 2, -1 ), 5 ) + self.assertEqual( w.dataToColumnIndex( 3, -1 ), 6 ) + self.assertEqual( w.dataToColumnIndex( 4, 0 ), 7 ) + self.assertEqual( w.dataToColumnIndex( 4, 1 ), 8 ) + self.assertEqual( w.dataToColumnIndex( 4, 2 ), 9 ) + + self.assertRaises( IndexError, w.dataToColumnIndex, 6, 0 ) + + def testColumnEditability( self ) : + + data = [ + IECore.FloatVectorData( range( 0, 3 ) ), + IECore.Color3fVectorData( [ imath.Color3f( x ) for x in range( 0, 3 ) ] ), + IECore.StringVectorData( [ str( x ) for x in range( 0, 3 ) ] ), + ] + + w = GafferUI.ArrayDataWidget( data ) + + for i in range( 0, 6 ) : + self.assertEqual( w.getColumnEditable( i ), True ) + + self.assertRaises( IndexError, w.getColumnEditable, 7 ) + self.assertRaises( IndexError, w.getColumnEditable, -1 ) + + w.setColumnEditable( 1, False ) + self.assertEqual( w.getColumnEditable( 1 ), False ) + + data[0][0] += 1.0 + w.setData( data ) + + for i in range( 0, 6 ) : + self.assertEqual( w.getColumnEditable( i ), i != 1 ) + + cs = GafferTest.CapturingSlot( w.dataChangedSignal() ) + self.assertEqual( len( cs ), 0 ) + + w.setColumnEditable( 0, False ) + w.setColumnEditable( 1, True ) + + # changing editability shouldn't emit dataChangedSignal. + self.assertEqual( len( cs ), 0 ) + + def testRowEditability( self ) : + + data = [ IECore.IntVectorData( range( 0, 3 ) ) ] + + w = GafferUI.ArrayDataWidget() + w.setRowEditability( [ False, True, False ] ) + w.setData( data ) + + model = w._VectorDataWidget__model + + index0 = model.index( 0, 0 ) + index1 = model.index( 1, 0 ) + index2 = model.index( 2, 0 ) + + self.assertFalse( bool( model.flags( index0 ) & QtCore.Qt.ItemIsEditable ) ) + self.assertTrue( bool( model.flags( index1 ) & QtCore.Qt.ItemIsEditable ) ) + self.assertFalse( bool( model.flags( index2 ) & QtCore.Qt.ItemIsEditable ) ) + +if __name__ == "__main__": + unittest.main() diff --git a/python/GafferUITest/__init__.py b/python/GafferUITest/__init__.py index f8996613514..a9f4ce0eb22 100644 --- a/python/GafferUITest/__init__.py +++ b/python/GafferUITest/__init__.py @@ -135,6 +135,7 @@ from .PopupWindowTest import PopupWindowTest from .ColorChooserTest import ColorChooserTest from .ContextTrackerTest import ContextTrackerTest +from .ArrayDataWidgetTest import ArrayDataWidgetTest if __name__ == "__main__": unittest.main()