From 898b59da346bd1d18bd892b949af6f12256e967c Mon Sep 17 00:00:00 2001 From: John Haddon Date: Thu, 16 Apr 2026 21:19:07 +0100 Subject: [PATCH 1/7] ValuePlugBinding : Remove unused includes --- src/GafferBindings/ValuePlugBinding.cpp | 1 - src/GafferModule/ValuePlugBinding.cpp | 1 - 2 files changed, 2 deletions(-) diff --git a/src/GafferBindings/ValuePlugBinding.cpp b/src/GafferBindings/ValuePlugBinding.cpp index cafe28e9c20..920a9ccc808 100644 --- a/src/GafferBindings/ValuePlugBinding.cpp +++ b/src/GafferBindings/ValuePlugBinding.cpp @@ -45,7 +45,6 @@ #include "Gaffer/Context.h" #include "Gaffer/Metadata.h" #include "Gaffer/Node.h" -#include "Gaffer/Reference.h" #include "Gaffer/Spreadsheet.h" #include "Gaffer/ValuePlug.h" diff --git a/src/GafferModule/ValuePlugBinding.cpp b/src/GafferModule/ValuePlugBinding.cpp index 9cf00ddcf82..9f5ad8410a6 100644 --- a/src/GafferModule/ValuePlugBinding.cpp +++ b/src/GafferModule/ValuePlugBinding.cpp @@ -46,7 +46,6 @@ #include "Gaffer/ValuePlug.h" #include "Gaffer/Node.h" #include "Gaffer/Context.h" -#include "Gaffer/Reference.h" #include "Gaffer/Metadata.h" using namespace boost::python; From af89261161535336202fa4399a6f65f7a6f75a02 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Thu, 16 Apr 2026 11:14:03 +0100 Subject: [PATCH 2/7] SubGraph : Simplify `correspondingInput()` documentation While we do still support the previous more manual method of setting this up, we only want people to use the BoxOut method now, which is much easier to document. --- include/Gaffer/SubGraph.h | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/include/Gaffer/SubGraph.h b/include/Gaffer/SubGraph.h index 1363d47c560..e3423fc1a9d 100644 --- a/include/Gaffer/SubGraph.h +++ b/include/Gaffer/SubGraph.h @@ -63,11 +63,7 @@ class GAFFER_API SubGraph : public DependencyNode const BoolPlug *enabledPlug() const override; /// Implemented to allow a user to define a pass-through behaviour - /// by wiring the nodes inside this sub graph up appropriately. The - /// input to the output plug must be connected from a node inside - /// the sub graph, where that node itself has its enabled plug driven - /// by the external enabled plug, and the correspondingInput for the - /// node comes from one of the inputs to the sub graph. + /// by connecting to the `passThrough` plug of an internal BoxOut node. Plug *correspondingInput( const Plug *output ) override; const Plug *correspondingInput( const Plug *output ) const override; From 0950ce01ac99999ae1710d73d2544b6708e57293 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Thu, 26 Mar 2026 17:47:40 +0000 Subject: [PATCH 3/7] SubGraph : Adopt reference loading features from Reference The long term plan here is to remove Reference entirely, with SubGraph being able to support both referencing and local modes. --- include/Gaffer/Reference.h | 28 +- include/Gaffer/SubGraph.h | 56 +++ src/Gaffer/Reference.cpp | 702 +------------------------- src/Gaffer/SubGraph.cpp | 715 +++++++++++++++++++++++++++ src/GafferModule/SubGraphBinding.cpp | 33 +- 5 files changed, 812 insertions(+), 722 deletions(-) diff --git a/include/Gaffer/Reference.h b/include/Gaffer/Reference.h index 5181c9b2f62..23c97ce97fa 100644 --- a/include/Gaffer/Reference.h +++ b/include/Gaffer/Reference.h @@ -45,6 +45,7 @@ namespace Gaffer IE_CORE_FORWARDDECLARE( StringPlug ) +/// \deprecated Use Box instead. class GAFFER_API Reference : public SubGraph { @@ -55,40 +56,21 @@ class GAFFER_API Reference : public SubGraph GAFFER_NODE_DECLARE_TYPE( Gaffer::Reference, ReferenceTypeId, SubGraph ); - /// Loads the specified script, which should have been exported - /// using Box::exportForReference(). - /// \undoable. + /// \deprecated Use `SubGraph::loadReference()` instead. void load( const std::filesystem::path &fileName ); - /// Returns the name of the script currently being referenced. + /// \deprecated Use `SubGraph::referenceFileName()` instead. const std::filesystem::path &fileName() const; + /// \deprecated Use `SubGraph::referenceChangedSignal()` instead. using ReferenceLoadedSignal = Signals::Signal; - /// Emitted when a reference is loaded (or unloaded following an undo). ReferenceLoadedSignal &referenceLoadedSignal(); - /// Edits - /// ===== - /// - /// Edits are changes to referenced plugs that are made by the user - /// after the reference has been loaded via `load()`. The Reference - /// node provides some limited tracking of edits, exposing them - /// via the following methods. - - bool hasMetadataEdit( const Plug *plug, const IECore::InternedString key ) const; - /// Returns true if `plug` has been added as a child of a referenced plug. - bool isChildEdit( const Plug *plug ) const; - private : - void loadInternal( const std::filesystem::path &fileName ); - bool isReferencePlug( const Plug *plug ) const; + void referenceChanged(); - std::filesystem::path m_fileName; ReferenceLoadedSignal m_referenceLoadedSignal; - class PlugEdits; - std::unique_ptr m_plugEdits; - }; IE_CORE_DECLAREPTR( Reference ) diff --git a/include/Gaffer/SubGraph.h b/include/Gaffer/SubGraph.h index e3423fc1a9d..89b1e4db707 100644 --- a/include/Gaffer/SubGraph.h +++ b/include/Gaffer/SubGraph.h @@ -38,6 +38,8 @@ #include "Gaffer/DependencyNode.h" +#include + namespace Gaffer { @@ -55,6 +57,46 @@ class GAFFER_API SubGraph : public DependencyNode GAFFER_NODE_DECLARE_TYPE( Gaffer::SubGraph, SubGraphTypeId, DependencyNode ); + /// Referencing + /// ----------- + /// + /// By default, a SubGraph's internal nodes are considered to be local, + /// meaning that they are serialised into the same `.gfr` file as the + /// SubGraph itself. In this state, the child nodes are user-editable. + /// Alternatively, SubGraphs may reference child nodes stored externally + /// in a separate `.grf` file. This allows self-contained node graphs + /// to be published and then referenced into multiple `.gfr` files. When + /// referencing, the child nodes are not user-editable. + + /// Loads a previously exported `.grf` file, replacing the internal node graph. + void loadReference( const std::filesystem::path &fileName ); + + /// Returns the referenced file. If `isReference()` is false, returns an + /// empty path. + const std::filesystem::path &referenceFileName() const; + + using ReferenceChangedSignal = Signals::Signal; + /// Emitted when `referenceFileName()` changes, or when a reference + /// is reloaded. + ReferenceChangedSignal &referenceChangedSignal(); + + /// Reference Edits + /// =============== + /// + /// Although child nodes can not be edited when referenced, promoted + /// plugs can be. This allows the user to control the internal network + /// via an interface defined when the reference was published. + /// + /// The SubGraph node provides some - currently limited - tracking of + //// such edits, exposing them via the following methods. + + bool hasMetadataEdit( const Plug *plug, const IECore::InternedString key ) const; + /// Returns true if `plug` has been added as a child of a referenced plug. + bool isChildEdit( const Plug *plug ) const; + + /// DependencyNode API + /// ------------------ + /// Does nothing void affects( const Plug *input, AffectedPlugsContainer &outputs ) const override; @@ -67,6 +109,20 @@ class GAFFER_API SubGraph : public DependencyNode Plug *correspondingInput( const Plug *output ) override; const Plug *correspondingInput( const Plug *output ) const override; + private : + + void loadReferenceInternal( const std::filesystem::path &fileName ); + bool isReferencePlug( const Plug *plug ) const; + + ReferenceChangedSignal m_referenceChangedSignal; + + class PlugEdits; + struct ReferenceState; + // Initialised lazily, when we first load a reference. + std::unique_ptr m_referenceState; + }; +IE_CORE_DECLAREPTR( SubGraph ) + } // namespace Gaffer diff --git a/src/Gaffer/Reference.cpp b/src/Gaffer/Reference.cpp index 0cc3e391ffb..566f1c94a0b 100644 --- a/src/Gaffer/Reference.cpp +++ b/src/Gaffer/Reference.cpp @@ -36,482 +36,18 @@ #include "Gaffer/Reference.h" -#include "Gaffer/Metadata.h" -#include "Gaffer/MetadataAlgo.h" -#include "Gaffer/PlugAlgo.h" -#include "Gaffer/ScriptNode.h" -#include "Gaffer/StandardSet.h" -#include "Gaffer/RampPlug.h" -#include "Gaffer/Spreadsheet.h" -#include "Gaffer/StringPlug.h" - -#include "IECore/Exception.h" -#include "IECore/MessageHandler.h" -#include "IECore/SearchPath.h" - -#include "boost/algorithm/string/predicate.hpp" #include "boost/bind/bind.hpp" -#include "boost/container/flat_set.hpp" - -#include "fmt/format.h" - -#include using namespace std; -using namespace boost::placeholders; using namespace IECore; using namespace Gaffer; -////////////////////////////////////////////////////////////////////////// -// Internal utilities -////////////////////////////////////////////////////////////////////////// - -namespace -{ - -bool descendantHasInput( const Plug *plug ) -{ - for( auto &d : Plug::RecursiveRange( *plug ) ) - { - if( d->getInput() ) - { - return true; - } - } - return false; -} - -bool conformRampPlugs( const Gaffer::Plug *srcPlug, Gaffer::Plug *dstPlug, bool ignoreDefaultValues ) -{ - auto conform = [=] ( auto typedSrc, Gaffer::Plug *dst ) { - - using PlugType = std::remove_const_t>; - auto typedDest = runTimeCast( dst ); - if( !typedDest ) - { - return false; - } - - if( typedSrc->isSetToDefault() && ignoreDefaultValues && !descendantHasInput( typedSrc ) ) - { - // We don't want to transfer any inputs or values, so must leave - // `dstPlug` alone. - return false; - } - - typedDest->clearPoints(); - for( size_t i = 0, n = typedSrc->numPoints(); i < n; ++i ) - { - const Plug *point = typedSrc->pointPlug( i ); - typedDest->addChild( point->createCounterpart( point->getName(), point->direction() ) ); - } - return true; - }; - - switch( (Gaffer::TypeId)srcPlug->typeId() ) - { - case RampffPlugTypeId : - return conform( static_cast( srcPlug ), dstPlug ); - case RampfColor3fPlugTypeId : - return conform( static_cast( srcPlug ), dstPlug ); - case RampfColor4fPlugTypeId : - return conform( static_cast( srcPlug ), dstPlug ); - default : - return false; - } -} - -/// \todo Consider moving to PlugAlgo.h -void copyInputsAndValues( Gaffer::Plug *srcPlug, Gaffer::Plug *dstPlug, bool ignoreDefaultValues ) -{ - - // From a user's perspective, we consider RampPlugs to have a single - // atomic value. So _any_ edit to _any_ child plug should cause the entire - // value to be matched. To do that, we first need to conform the destination - // so that it has the same number of points as the source, and then we need - // to set values for all plugs. - - if( conformRampPlugs( srcPlug, dstPlug, ignoreDefaultValues ) ) - { - ignoreDefaultValues = false; - } - - // If we have an input to copy, we can leave the - // recursion to the `setInput()` call, which will - // also set all descendant inputs. - - if( Plug *input = srcPlug->getInput() ) - { - dstPlug->setInput( input ); - return; - } - - // We have no input. - // ================= - - // If we're at a leaf plug, remove the destination - // input and copy the value. - - if( !dstPlug->children().size() ) - { - dstPlug->setInput( nullptr ); - if( ValuePlug *srcValuePlug = runTimeCast( srcPlug ) ) - { - if( !ignoreDefaultValues || !srcValuePlug->isSetToDefault() ) - { - if( ValuePlug *dstValuePlug = runTimeCast( dstPlug ) ) - { - dstValuePlug->setFrom( srcValuePlug ); - } - } - } - return; - } - - // Otherwise, recurse to children. We recurse awkwardly - // using indices rather than PlugIterator for compatibility - // with ArrayPlug, which will add new children as inputs are - // added. - - const Plug::ChildContainer &children = dstPlug->children(); - for( size_t i = 0; i < children.size(); ++i ) - { - if( Plug *srcChildPlug = srcPlug->getChild( children[i]->getName() ) ) - { - copyInputsAndValues( srcChildPlug, static_cast( children[i].get() ), ignoreDefaultValues ); - } - } - -} - -/// \todo Consider moving to PlugAlgo.h -void transferOutputs( Gaffer::Plug *srcPlug, Gaffer::Plug *dstPlug ) -{ - // Transfer outputs - - for( Plug::OutputContainer::const_iterator oIt = srcPlug->outputs().begin(), oeIt = srcPlug->outputs().end(); oIt != oeIt; ) - { - Plug *outputPlug = *oIt; - ++oIt; // increment now because the setInput() call invalidates our iterator. - outputPlug->setInput( dstPlug ); - } - - // Recurse - - for( Plug::Iterator it( srcPlug ); !it.done(); ++it ) - { - if( Plug *dstChildPlug = dstPlug->getChild( (*it)->getName() ) ) - { - transferOutputs( it->get(), dstChildPlug ); - } - } -} - -const InternedString g_childNodesAreReadOnlyName( "childNodesAreReadOnly" ); - -} // namespace - -////////////////////////////////////////////////////////////////////////// -// PlugEdits. This internal utility class is used to track where edits have -// been applied to plugs following loading. -////////////////////////////////////////////////////////////////////////// - -class Reference::PlugEdits : public Signals::Trackable -{ - - public : - - PlugEdits( Reference *reference ) - : m_reference( reference ) - { - m_connection = Metadata::plugValueChangedSignal( reference ).connect( boost::bind( &PlugEdits::plugValueChanged, this, ::_1, ::_2, ::_3 ) ); - m_reference->childRemovedSignal().connect( boost::bind( &PlugEdits::childRemoved, this, ::_1, ::_2 ) ); - } - - bool hasMetadataEdit( const Plug *plug, const InternedString &key ) const - { - const PlugEdit *edit = plugEdit( plug ); - - if( !edit ) - { - return false; - } - - return edit->metadataEdits.find( key ) != edit->metadataEdits.end(); - } - - bool isChildEdit( const Plug *plug ) const - { - const Plug *parent = plug->parent(); - if( !parent ) - { - return false; - } - - const PlugEdit *edit = plugEdit( parent ); - if( !edit ) - { - return false; - } - - if( edit->sizeAfterLoad == -1 || parent->children().size() <= (size_t)edit->sizeAfterLoad ) - { - return false; - } - - // Conceptually we want to compare the index of `plug` against - // `sizeAfterLoad`. But finding the index currently requires linear - // search. We expect the UI to only allow creation of new plugs in - // originally-empty containers (to avoid merge hell on reload), - // meaning that `sizeAfterLoad` can be expected to be either 0 or 1 - // (the latter for RowsPlug with a default row). So it is quicker to - // reverse the test and search for plug in the range `[0, - // sizeAfterLoad)`. - return !std::any_of( - parent->children().begin(), parent->children().begin() + edit->sizeAfterLoad, - [plug]( const GraphComponentPtr &child ) { return child == plug; } - ); - } - - void transferEdits( Plug *oldPlug, Plug *newPlug ) const - { - transferEditedMetadata( oldPlug, newPlug ); - transferChildEdits( oldPlug, newPlug ); - } - - // Used to allow PlugEdits to track reference loading. - struct LoadingScope : boost::noncopyable - { - LoadingScope( PlugEdits *plugEdits ) - : m_plugEdits( plugEdits ), m_blockedConnection( plugEdits->m_connection ) - { - } - ~LoadingScope() - { - m_plugEdits->loadingFinished(); - } - private : - PlugEdits *m_plugEdits; - // Changes made during loading aren't user edits and mustn't be - // tracked, so we block the connection. - Signals::BlockedConnection m_blockedConnection; - }; - - private : - - Reference *m_reference; - Signals::ScopedConnection m_connection; - - // Struct for tracking all edits to a plug, where an edit is conceptually - // any change the user makes to the plug after it has been loaded by - // `Reference::load()`. In practice we currently only track metadata - // edits and the addition of children to a subset of plug types. - struct PlugEdit - { - boost::container::flat_set metadataEdits; - int64_t sizeAfterLoad = -1; // Default value means size not tracked - }; - - std::unordered_map m_plugEdits; - - const PlugEdit *plugEdit( const Plug *plug ) const - { - // Cheeky cast better than maintaining two near-identical functions. - return const_cast( this )->plugEdit( plug, /* createIfMissing = */ false ); - } - - PlugEdit *plugEdit( const Plug *plug, bool createIfMissing ) - { - if( plug->node() != m_reference ) - { - return nullptr; - } - - auto it = m_plugEdits.find( plug ); - if( it != m_plugEdits.end() ) - { - return &(it->second); - } - - if( !m_reference->isReferencePlug( plug ) ) - { - // We'll allow retrieval of existing edits on this plug, but we - // won't create new ones. - return nullptr; - } - - if( !createIfMissing ) - { - return nullptr; - } - - return &m_plugEdits[plug]; - } - - void plugValueChanged( const Gaffer::Plug *plug, IECore::InternedString key, Metadata::ValueChangedReason reason ) - { - if( - reason == Metadata::ValueChangedReason::StaticRegistration || - reason == Metadata::ValueChangedReason::StaticDeregistration - ) - { - return; - } - - ScriptNode *scriptNode = m_reference->ancestor(); - if( scriptNode && ( scriptNode->currentActionStage() == Action::Undo || scriptNode->currentActionStage() == Action::Redo ) ) - { - // Our edit tracking code below utilises the undo system, so we don't need - // to do anything for an Undo or Redo - our action from the original Do will - // be replayed automatically. - return; - } - - PlugEdit *edit = plugEdit( plug, /* createIfMissing = */ true ); - if( !edit ) - { - // May get a null edit even with `createIfMissing = true`, - // if the plug is not a reference plug node. - return; - } - - if( edit->metadataEdits.find( key ) != edit->metadataEdits.end() ) - { - return; - } - - Action::enact( - m_reference, - [edit, key](){ edit->metadataEdits.insert( key ); }, - [edit, key](){ edit->metadataEdits.erase( key ); } - ); - } - - void childRemoved( GraphComponent *parent, GraphComponent *child ) - { - const Plug *plug = runTimeCast( child ); - if( !plug ) - { - return; - } - - for( Plug::RecursiveIterator it( plug ); !it.done(); ++it ) - { - m_plugEdits.erase( it->get() ); - } - - m_plugEdits.erase( plug ); - } - - void loadingFinished() - { - for( auto &plug : Plug::RecursiveRange( *m_reference ) ) - { - if( !m_reference->isReferencePlug( plug.get() ) ) - { - continue; - } - - const IECore::TypeId plugType = plug->typeId(); - if( - plugType != (IECore::TypeId)SpreadsheetRowsPlugTypeId && - plugType != (IECore::TypeId)CompoundDataPlugTypeId - ) - { - // We only support child edits for RowsPlugs and - // CompoundDataPlugs at present. It would be trivial - // to do the tracking for everything, but most types - // don't have dynamic numbers of children, and we - // probably don't want the overhead of a PlugEdit for - // everything else. - continue; - } - if( auto *edit = plugEdit( plug.get(), /* createIfMissing = */ true ) ) - { - edit->sizeAfterLoad = plug->children().size(); - } - } - } - - void transferEditedMetadata( const Plug *srcPlug, Plug *dstPlug ) const - { - // Transfer metadata that was edited and won't be provided by a - // load. Note: Adding the metadata to a new plug - // automatically registers a PlugEdit for that plug. - - if( auto *edit = plugEdit( srcPlug ) ) - { - for( const InternedString &key : edit->metadataEdits ) - { - Gaffer::Metadata::registerValue( dstPlug, key, Gaffer::Metadata::value( srcPlug, key ), /* persistent =*/ true ); - } - } - - // Recurse - - for( Plug::Iterator it( srcPlug ); !it.done(); ++it ) - { - if( Plug *dstChildPlug = dstPlug->getChild( (*it)->getName() ) ) - { - transferEditedMetadata( it->get(), dstChildPlug ); - } - } - } - - void transferChildEdits( Plug *oldPlug, Plug *newPlug ) const - { - if( newPlug->typeId() != oldPlug->typeId() ) - { - return; - } - - // Recurse - - for( Plug::Iterator it( oldPlug ); !it.done(); ++it ) - { - if( Plug *dstChildPlug = newPlug->getChild( (*it)->getName() ) ) - { - transferChildEdits( it->get(), dstChildPlug ); - } - } - - auto *edit = plugEdit( oldPlug ); - if( !edit || edit->sizeAfterLoad == -1 ) - { - return; - } - - auto *newRows = runTimeCast( newPlug ); - for( size_t i = edit->sizeAfterLoad; i < oldPlug->children().size(); ++i ) - { - if( newRows ) - { - // The only valid way to add children to a RowsPlug is to - // call `addRow()`. If we don't use that, our new rows may - // have the wrong number of columns if the columns in the - // referenced file have been changed. - Spreadsheet::RowPlug *newRow = newRows->addRow(); - newRow->setName( oldPlug->getChild( i )->getName() ); - } - else - { - const Plug *oldChild = oldPlug->getChild( i ); - newPlug->addChild( oldChild->createCounterpart( oldChild->getName(), oldChild->direction() ) ); - } - } - } - -}; - -////////////////////////////////////////////////////////////////////////// -// Reference -////////////////////////////////////////////////////////////////////////// - GAFFER_NODE_DEFINE_TYPE( Reference ); Reference::Reference( const std::string &name ) - : SubGraph( name ), m_plugEdits( new PlugEdits( this ) ) + : SubGraph( name ) { + referenceChangedSignal().connect( boost::bind( &Reference::referenceChanged, this ) ); } Reference::~Reference() @@ -520,31 +56,12 @@ Reference::~Reference() void Reference::load( const std::filesystem::path &fileName ) { - const char *s = getenv( "GAFFER_REFERENCE_PATHS" ); - IECore::SearchPath sp( s ? s : "" ); - /// \todo Convert SearchPath to deal in `std::filesystem` rather than `boost::filesystem`. - std::filesystem::path path = sp.find( fileName.string() ).string(); - if( path.empty() ) - { - throw Exception( "Could not find file '" + fileName.generic_string() + "'" ); - } - - ScriptNode *script = scriptNode(); - if( !script ) - { - throw IECore::Exception( "Reference::load called without ScriptNode" ); - } - - Action::enact( - this, - boost::bind( &Reference::loadInternal, ReferencePtr( this ), fileName ), - boost::bind( &Reference::loadInternal, ReferencePtr( this ), m_fileName ) - ); + SubGraph::loadReference( fileName ); } const std::filesystem::path &Reference::fileName() const { - return m_fileName; + return SubGraph::referenceFileName(); } Reference::ReferenceLoadedSignal &Reference::referenceLoadedSignal() @@ -552,214 +69,7 @@ Reference::ReferenceLoadedSignal &Reference::referenceLoadedSignal() return m_referenceLoadedSignal; } -void Reference::loadInternal( const std::filesystem::path &fileName ) -{ - ScriptNode *script = scriptNode(); - - // Disable undo for the actions we perform, because we ourselves - // are undoable anyway and will take care of everything as a whole - // when we are undone. - UndoScope undoDisabler( script, UndoScope::Disabled ); - - // if we're doing a reload, then we want to maintain any values and - // connections that our external plugs might have. but we also need to - // get those existing plugs out of the way during the load, so that the - // incoming plugs don't get renamed. - - std::map previousPlugs; - for( Plug::Iterator it( this ); !it.done(); ++it ) - { - Plug *plug = it->get(); - if( isReferencePlug( plug ) ) - { - previousPlugs[plug->getName()] = plug; - plug->setName( "__tmp__" + plug->getName().string() ); - } - } - - // We don't export user plugs to references, but old versions of - // Gaffer did, so as above, we must get them out of the way during - // the load. - for( Plug::Iterator it( userPlug() ); !it.done(); ++it ) - { - Plug *plug = it->get(); - if( isReferencePlug( plug ) ) - { - previousPlugs[plug->relativeName( this )] = plug; - plug->setName( "__tmp__" + plug->getName().string() ); - } - } - - // if we're doing a reload, then we also need to delete all our child - // nodes to make way for the incoming nodes. - - int i = (int)(children().size()) - 1; - while( i >= 0 ) - { - if( Node *node = getChild( i ) ) - { - removeChild( node ); - } - i--; - } - - // Set up a container to catch all the children added during loading. - StandardSetPtr newChildren = new StandardSet; - childAddedSignal().connect( boost::bind( (bool (StandardSet::*)( IECore::RunTimeTypedPtr ) )&StandardSet::add, newChildren.get(), ::_2 ) ); - userPlug()->childAddedSignal().connect( boost::bind( (bool (StandardSet::*)( IECore::RunTimeTypedPtr ) )&StandardSet::add, newChildren.get(), ::_2 ) ); - - // load the reference. we use continueOnError=true to get everything possible - // loaded, but if any errors do occur we throw an exception at the end of this - // function. this means that the caller is still notified of errors via the - // exception mechanism, but we leave ourselves in the best state possible for - // the case where ScriptNode::load( continueOnError = true ) will ignore the - // exception that we throw. - - bool errors = false; - const char *s = getenv( "GAFFER_REFERENCE_PATHS" ); - IECore::SearchPath sp( s ? s : "" ); - /// \todo Convert SearchPath to deal in `std::filesystem` rather than `boost::filesystem`. - std::filesystem::path path = sp.find( fileName.string() ).string(); - if( !path.empty() ) - { - PlugEdits::LoadingScope loadingScope( m_plugEdits.get() ); - errors = script->executeFile( path.string(), this, /* continueOnError = */ true ); - // deregister "childNodesAreReadOnly" metadata, in case it was baked in the exported file - Metadata::deregisterValue( this, g_childNodesAreReadOnlyName ); - } - - // Do a little bit of post processing on everything that was loaded. - - for( size_t i = 0, e = newChildren->size(); i < e; ++i ) - { - if( Plug *plug = runTimeCast( newChildren->member( i ) ) ) - { - // Make the loaded plugs non-dynamic, because we don't want them - // to be serialised in the script the reference is in - the whole - // point is that they are referenced. - /// \todo Plug flags are not working. We need to introduce an - /// alternative mechanism based on querying parent nodes/plugs - /// for serialisation requirements at the point of serialisation. - plug->setFlags( Plug::Dynamic, false ); - - if( - runTimeCast( plug ) || - runTimeCast( plug ) || - runTimeCast( plug ) - ) - { - // Avoid recursion as it makes it impossible to serialise - // the `x/y` children of ramp points. See RampPlugSerialiser - // for further details of ramp serialisation. - continue; - } - - for( Plug::RecursiveIterator it( plug ); !it.done(); ++it ) - { - (*it)->setFlags( Plug::Dynamic, false ); - } - } - } - - // Transfer connections, values and metadata from the old plugs onto the corresponding new ones. - - for( std::map::const_iterator it = previousPlugs.begin(), eIt = previousPlugs.end(); it != eIt; ++it ) - { - Plug *oldPlug = it->second; - Plug *newPlug = descendant( it->first ); - if( newPlug ) - { - try - { - m_plugEdits->transferEdits( oldPlug, newPlug ); - if( newPlug->direction() == Plug::In && oldPlug->direction() == Plug::In ) - { - copyInputsAndValues( oldPlug, newPlug, /* ignoreDefaultValues = */ true ); - } - transferOutputs( oldPlug, newPlug ); - } - catch( const std::exception &e ) - { - msg( - Msg::Warning, - fmt::format( "Loading \"{}\" onto \"{}\"", fileName.generic_string(), getName().c_str() ), - e.what() - ); - } - - } - - // remove the old plug now we're done with it. - oldPlug->parent()->removeChild( oldPlug ); - } - - // Finish up. - - m_fileName = fileName; - referenceLoadedSignal()( this ); - - if( errors ) - { - throw Exception( fmt::format( "Error loading reference \"{}\"", fileName.generic_string() ) ); - } - -} - -bool Reference::hasMetadataEdit( const Plug *plug, const IECore::InternedString key ) const -{ - return m_plugEdits->hasMetadataEdit( plug, key ); -} - -bool Reference::isChildEdit( const Plug *plug ) const +void Reference::referenceChanged() { - return m_plugEdits->isChildEdit( plug ); -} - -bool Reference::isReferencePlug( const Plug *plug ) const -{ - // If a plug is the descendant of a plug starting with - // __, and that plug is a direct child of the reference, - // assume that it is for gaffer's internal use, so would - // never come directly from a reference. This lines up - // with the export code in Box::exportForReference(), where - // such plugs are excluded from the export. - - // find ancestor of p which is a direct child of this node: - const Plug* ancestorPlug = plug; - const GraphComponent* parent = plug->parent(); - while( parent != this ) - { - ancestorPlug = runTimeCast< const Plug >( parent ); - if( !ancestorPlug ) - { - // Looks like the plug we're looking for doesn't exist, - // so we exit the loop. - break; - } - parent = ancestorPlug->parent(); - } - - if( ancestorPlug && boost::starts_with( ancestorPlug->getName().c_str(), "__" ) ) - { - return false; - } - - // we know this doesn't come from a reference, - // because it's made during construction. - if( plug == userPlug() ) - { - return false; - } - - // User plugs are not meant to be referenced either. But old - // versions of Gaffer did export them so we must be careful. - // Since we make loaded plugs non-dynamic, we can assume that - // if the plug is dynamic it was added locally by a user - // rather than loaded from a reference. - if( ancestorPlug == userPlug() && plug->getFlags( Plug::Dynamic ) ) - { - return false; - } - // everything else must be from a reference then. - return true; + m_referenceLoadedSignal( this ); } diff --git a/src/Gaffer/SubGraph.cpp b/src/Gaffer/SubGraph.cpp index 9ace797b51e..378a9ac36ef 100644 --- a/src/Gaffer/SubGraph.cpp +++ b/src/Gaffer/SubGraph.cpp @@ -36,11 +36,475 @@ #include "Gaffer/SubGraph.h" +#include "Gaffer/Action.h" #include "Gaffer/BoxOut.h" +#include "Gaffer/Metadata.h" +#include "Gaffer/RampPlug.h" +#include "Gaffer/ScriptNode.h" +#include "Gaffer/Spreadsheet.h" +#include "Gaffer/StandardSet.h" +#include "Gaffer/UndoScope.h" + +#include "IECore/SearchPath.h" + +#include "boost/algorithm/string/predicate.hpp" +#include "boost/bind.hpp" using namespace IECore; using namespace Gaffer; +////////////////////////////////////////////////////////////////////////// +// Internal utilities +////////////////////////////////////////////////////////////////////////// + +namespace +{ + +bool descendantHasInput( const Plug *plug ) +{ + for( auto &d : Plug::RecursiveRange( *plug ) ) + { + if( d->getInput() ) + { + return true; + } + } + return false; +} + +bool conformRampPlugs( const Gaffer::Plug *srcPlug, Gaffer::Plug *dstPlug, bool ignoreDefaultValues ) +{ + auto conform = [=] ( auto typedSrc, Gaffer::Plug *dst ) { + + using PlugType = std::remove_const_t>; + auto typedDest = runTimeCast( dst ); + if( !typedDest ) + { + return false; + } + + if( typedSrc->isSetToDefault() && ignoreDefaultValues && !descendantHasInput( typedSrc ) ) + { + // We don't want to transfer any inputs or values, so must leave + // `dstPlug` alone. + return false; + } + + typedDest->clearPoints(); + for( size_t i = 0, n = typedSrc->numPoints(); i < n; ++i ) + { + const Plug *point = typedSrc->pointPlug( i ); + typedDest->addChild( point->createCounterpart( point->getName(), point->direction() ) ); + } + return true; + }; + + switch( (Gaffer::TypeId)srcPlug->typeId() ) + { + case RampffPlugTypeId : + return conform( static_cast( srcPlug ), dstPlug ); + case RampfColor3fPlugTypeId : + return conform( static_cast( srcPlug ), dstPlug ); + case RampfColor4fPlugTypeId : + return conform( static_cast( srcPlug ), dstPlug ); + default : + return false; + } +} + +/// \todo Consider moving to PlugAlgo.h +void copyInputsAndValues( Gaffer::Plug *srcPlug, Gaffer::Plug *dstPlug, bool ignoreDefaultValues ) +{ + + // From a user's perspective, we consider RampPlugs to have a single + // atomic value. So _any_ edit to _any_ child plug should cause the entire + // value to be matched. To do that, we first need to conform the destination + // so that it has the same number of points as the source, and then we need + // to set values for all plugs. + + if( conformRampPlugs( srcPlug, dstPlug, ignoreDefaultValues ) ) + { + ignoreDefaultValues = false; + } + + // If we have an input to copy, we can leave the + // recursion to the `setInput()` call, which will + // also set all descendant inputs. + + if( Plug *input = srcPlug->getInput() ) + { + dstPlug->setInput( input ); + return; + } + + // We have no input. + // ================= + + // If we're at a leaf plug, remove the destination + // input and copy the value. + + if( !dstPlug->children().size() ) + { + dstPlug->setInput( nullptr ); + if( ValuePlug *srcValuePlug = runTimeCast( srcPlug ) ) + { + if( !ignoreDefaultValues || !srcValuePlug->isSetToDefault() ) + { + if( ValuePlug *dstValuePlug = runTimeCast( dstPlug ) ) + { + dstValuePlug->setFrom( srcValuePlug ); + } + } + } + return; + } + + // Otherwise, recurse to children. We recurse awkwardly + // using indices rather than PlugIterator for compatibility + // with ArrayPlug, which will add new children as inputs are + // added. + + const Plug::ChildContainer &children = dstPlug->children(); + for( size_t i = 0; i < children.size(); ++i ) + { + if( Plug *srcChildPlug = srcPlug->getChild( children[i]->getName() ) ) + { + copyInputsAndValues( srcChildPlug, static_cast( children[i].get() ), ignoreDefaultValues ); + } + } + +} + +/// \todo Consider moving to PlugAlgo.h +void transferOutputs( Gaffer::Plug *srcPlug, Gaffer::Plug *dstPlug ) +{ + // Transfer outputs + + for( Plug::OutputContainer::const_iterator oIt = srcPlug->outputs().begin(), oeIt = srcPlug->outputs().end(); oIt != oeIt; ) + { + Plug *outputPlug = *oIt; + ++oIt; // increment now because the setInput() call invalidates our iterator. + outputPlug->setInput( dstPlug ); + } + + // Recurse + + for( Plug::Iterator it( srcPlug ); !it.done(); ++it ) + { + if( Plug *dstChildPlug = dstPlug->getChild( (*it)->getName() ) ) + { + transferOutputs( it->get(), dstChildPlug ); + } + } +} + +const InternedString g_childNodesAreReadOnlyName( "childNodesAreReadOnly" ); +const std::filesystem::path g_emptyPath; + +} // namespace + +////////////////////////////////////////////////////////////////////////// +// PlugEdits and ReferenceState +////////////////////////////////////////////////////////////////////////// + +class SubGraph::PlugEdits : public Signals::Trackable +{ + + public : + + PlugEdits( SubGraph *subGraph ) + : m_subGraph( subGraph ) + { + m_connection = Metadata::plugValueChangedSignal( subGraph ).connect( boost::bind( &PlugEdits::plugValueChanged, this, ::_1, ::_2, ::_3 ) ); + m_subGraph->childRemovedSignal().connect( boost::bind( &PlugEdits::childRemoved, this, ::_1, ::_2 ) ); + } + + bool hasMetadataEdit( const Plug *plug, const InternedString &key ) const + { + const PlugEdit *edit = plugEdit( plug ); + + if( !edit ) + { + return false; + } + + return edit->metadataEdits.find( key ) != edit->metadataEdits.end(); + } + + bool isChildEdit( const Plug *plug ) const + { + const Plug *parent = plug->parent(); + if( !parent ) + { + return false; + } + + const PlugEdit *edit = plugEdit( parent ); + if( !edit ) + { + return false; + } + + if( edit->sizeAfterLoad == -1 || parent->children().size() <= (size_t)edit->sizeAfterLoad ) + { + return false; + } + + // Conceptually we want to compare the index of `plug` against + // `sizeAfterLoad`. But finding the index currently requires linear + // search. We expect the UI to only allow creation of new plugs in + // originally-empty containers (to avoid merge hell on reload), + // meaning that `sizeAfterLoad` can be expected to be either 0 or 1 + // (the latter for RowsPlug with a default row). So it is quicker to + // reverse the test and search for plug in the range `[0, + // sizeAfterLoad)`. + return !std::any_of( + parent->children().begin(), parent->children().begin() + edit->sizeAfterLoad, + [plug]( const GraphComponentPtr &child ) { return child == plug; } + ); + } + + void transferEdits( Plug *oldPlug, Plug *newPlug ) const + { + transferEditedMetadata( oldPlug, newPlug ); + transferChildEdits( oldPlug, newPlug ); + } + + // Used to allow PlugEdits to track reference loading. + struct LoadingScope : boost::noncopyable + { + LoadingScope( PlugEdits &plugEdits ) + : m_plugEdits( plugEdits ), m_blockedConnection( plugEdits.m_connection ) + { + } + ~LoadingScope() + { + m_plugEdits.loadingFinished(); + } + private : + PlugEdits &m_plugEdits; + // Changes made during loading aren't user edits and mustn't be + // tracked, so we block the connection. + Signals::BlockedConnection m_blockedConnection; + }; + + private : + + SubGraph *m_subGraph; + Signals::ScopedConnection m_connection; + + // Struct for tracking all edits to a plug, where an edit is conceptually + // any change the user makes to the plug after it has been loaded by + // `SubGraph::loadReference()`. In practice we currently only track metadata + // edits and the addition of children to a subset of plug types. + struct PlugEdit + { + boost::container::flat_set metadataEdits; + int64_t sizeAfterLoad = -1; // Default value means size not tracked + }; + + std::unordered_map m_plugEdits; + + const PlugEdit *plugEdit( const Plug *plug ) const + { + // Cheeky cast better than maintaining two near-identical functions. + return const_cast( this )->plugEdit( plug, /* createIfMissing = */ false ); + } + + PlugEdit *plugEdit( const Plug *plug, bool createIfMissing ) + { + if( plug->node() != m_subGraph ) + { + return nullptr; + } + + auto it = m_plugEdits.find( plug ); + if( it != m_plugEdits.end() ) + { + return &(it->second); + } + + if( !m_subGraph->isReferencePlug( plug ) ) + { + // We'll allow retrieval of existing edits on this plug, but we + // won't create new ones. + return nullptr; + } + + if( !createIfMissing ) + { + return nullptr; + } + + return &m_plugEdits[plug]; + } + + void plugValueChanged( const Gaffer::Plug *plug, IECore::InternedString key, Metadata::ValueChangedReason reason ) + { + if( + reason == Metadata::ValueChangedReason::StaticRegistration || + reason == Metadata::ValueChangedReason::StaticDeregistration + ) + { + return; + } + + ScriptNode *scriptNode = m_subGraph->ancestor(); + if( scriptNode && ( scriptNode->currentActionStage() == Action::Undo || scriptNode->currentActionStage() == Action::Redo ) ) + { + // Our edit tracking code below utilises the undo system, so we don't need + // to do anything for an Undo or Redo - our action from the original Do will + // be replayed automatically. + return; + } + + PlugEdit *edit = plugEdit( plug, /* createIfMissing = */ true ); + if( !edit ) + { + // May get a null edit even with `createIfMissing = true`, + // if the plug is not a reference plug node. + return; + } + + if( edit->metadataEdits.find( key ) != edit->metadataEdits.end() ) + { + return; + } + + Action::enact( + m_subGraph, + [edit, key](){ edit->metadataEdits.insert( key ); }, + [edit, key](){ edit->metadataEdits.erase( key ); } + ); + } + + void childRemoved( GraphComponent *parent, GraphComponent *child ) + { + const Plug *plug = runTimeCast( child ); + if( !plug ) + { + return; + } + + for( Plug::RecursiveIterator it( plug ); !it.done(); ++it ) + { + m_plugEdits.erase( it->get() ); + } + + m_plugEdits.erase( plug ); + } + + void loadingFinished() + { + for( auto &plug : Plug::RecursiveRange( *m_subGraph ) ) + { + if( !m_subGraph->isReferencePlug( plug.get() ) ) + { + continue; + } + + const IECore::TypeId plugType = plug->typeId(); + if( + plugType != (IECore::TypeId)SpreadsheetRowsPlugTypeId && + plugType != (IECore::TypeId)CompoundDataPlugTypeId + ) + { + // We only support child edits for RowsPlugs and + // CompoundDataPlugs at present. It would be trivial + // to do the tracking for everything, but most types + // don't have dynamic numbers of children, and we + // probably don't want the overhead of a PlugEdit for + // everything else. + continue; + } + if( auto *edit = plugEdit( plug.get(), /* createIfMissing = */ true ) ) + { + edit->sizeAfterLoad = plug->children().size(); + } + } + } + + void transferEditedMetadata( const Plug *srcPlug, Plug *dstPlug ) const + { + // Transfer metadata that was edited and won't be provided by a + // load. Note: Adding the metadata to a new plug + // automatically registers a PlugEdit for that plug. + + if( auto *edit = plugEdit( srcPlug ) ) + { + for( const InternedString &key : edit->metadataEdits ) + { + Gaffer::Metadata::registerValue( dstPlug, key, Gaffer::Metadata::value( srcPlug, key ), /* persistent =*/ true ); + } + } + + // Recurse + + for( Plug::Iterator it( srcPlug ); !it.done(); ++it ) + { + if( Plug *dstChildPlug = dstPlug->getChild( (*it)->getName() ) ) + { + transferEditedMetadata( it->get(), dstChildPlug ); + } + } + } + + void transferChildEdits( Plug *oldPlug, Plug *newPlug ) const + { + if( newPlug->typeId() != oldPlug->typeId() ) + { + return; + } + + // Recurse + + for( Plug::Iterator it( oldPlug ); !it.done(); ++it ) + { + if( Plug *dstChildPlug = newPlug->getChild( (*it)->getName() ) ) + { + transferChildEdits( it->get(), dstChildPlug ); + } + } + + auto *edit = plugEdit( oldPlug ); + if( !edit || edit->sizeAfterLoad == -1 ) + { + return; + } + + auto *newRows = runTimeCast( newPlug ); + for( size_t i = edit->sizeAfterLoad; i < oldPlug->children().size(); ++i ) + { + if( newRows ) + { + // The only valid way to add children to a RowsPlug is to + // call `addRow()`. If we don't use that, our new rows may + // have the wrong number of columns if the columns in the + // referenced file have been changed. + Spreadsheet::RowPlug *newRow = newRows->addRow(); + newRow->setName( oldPlug->getChild( i )->getName() ); + } + else + { + const Plug *oldChild = oldPlug->getChild( i ); + newPlug->addChild( oldChild->createCounterpart( oldChild->getName(), oldChild->direction() ) ); + } + } + } + +}; + +struct SubGraph::ReferenceState +{ + ReferenceState( SubGraph *subGraph ) : plugEdits( subGraph ) {} + std::filesystem::path fileName; + PlugEdits plugEdits; +}; + +////////////////////////////////////////////////////////////////////////// +// SubGraph +////////////////////////////////////////////////////////////////////////// + GAFFER_NODE_DEFINE_TYPE( SubGraph ); static IECore::InternedString g_enabledName( "enabled" ); @@ -54,6 +518,40 @@ SubGraph::~SubGraph() { } +void SubGraph::loadReference( const std::filesystem::path &fileName ) +{ + const char *s = getenv( "GAFFER_REFERENCE_PATHS" ); + IECore::SearchPath sp( s ? s : "" ); + /// \todo Convert SearchPath to deal in `std::filesystem` rather than `boost::filesystem`. + std::filesystem::path path = sp.find( fileName.string() ).string(); + if( path.empty() ) + { + throw Exception( "Could not find file '" + fileName.generic_string() + "'" ); + } + + ScriptNode *script = scriptNode(); + if( !script ) + { + throw IECore::Exception( "SubGraph::loadReference called without ScriptNode" ); + } + + Action::enact( + this, + boost::bind( &SubGraph::loadReferenceInternal, SubGraphPtr( this ), fileName ), + boost::bind( &SubGraph::loadReferenceInternal, SubGraphPtr( this ), referenceFileName() ) + ); +} + +const std::filesystem::path &SubGraph::referenceFileName() const +{ + return m_referenceState ? m_referenceState->fileName : g_emptyPath; +} + +SubGraph::ReferenceChangedSignal &SubGraph::referenceChangedSignal() +{ + return m_referenceChangedSignal; +} + void SubGraph::affects( const Plug *input, AffectedPlugsContainer &outputs ) const { DependencyNode::affects( input, outputs ); @@ -147,4 +645,221 @@ const Plug *SubGraph::correspondingInput( const Plug *output ) const } return nullptr; + } + +void SubGraph::loadReferenceInternal( const std::filesystem::path &fileName ) +{ + if( !m_referenceState ) + { + m_referenceState = std::make_unique( this ); + } + + ScriptNode *script = scriptNode(); + + // Disable undo for the actions we perform, because we ourselves + // are undoable anyway and will take care of everything as a whole + // when we are undone. + UndoScope undoDisabler( script, UndoScope::Disabled ); + + // if we're doing a reload, then we want to maintain any values and + // connections that our external plugs might have. but we also need to + // get those existing plugs out of the way during the load, so that the + // incoming plugs don't get renamed. + + std::map previousPlugs; + for( Plug::Iterator it( this ); !it.done(); ++it ) + { + Plug *plug = it->get(); + if( isReferencePlug( plug ) ) + { + previousPlugs[plug->getName()] = plug; + plug->setName( "__tmp__" + plug->getName().string() ); + } + } + + // We don't export user plugs to references, but old versions of + // Gaffer did, so as above, we must get them out of the way during + // the load. + for( Plug::Iterator it( userPlug() ); !it.done(); ++it ) + { + Plug *plug = it->get(); + if( isReferencePlug( plug ) ) + { + previousPlugs[plug->relativeName( this )] = plug; + plug->setName( "__tmp__" + plug->getName().string() ); + } + } + + // if we're doing a reload, then we also need to delete all our child + // nodes to make way for the incoming nodes. + + int i = (int)(children().size()) - 1; + while( i >= 0 ) + { + if( Node *node = getChild( i ) ) + { + removeChild( node ); + } + i--; + } + + // Set up a container to catch all the children added during loading. + StandardSetPtr newChildren = new StandardSet; + childAddedSignal().connect( boost::bind( (bool (StandardSet::*)( IECore::RunTimeTypedPtr ) )&StandardSet::add, newChildren.get(), ::_2 ) ); + userPlug()->childAddedSignal().connect( boost::bind( (bool (StandardSet::*)( IECore::RunTimeTypedPtr ) )&StandardSet::add, newChildren.get(), ::_2 ) ); + + // load the reference. we use continueOnError=true to get everything possible + // loaded, but if any errors do occur we throw an exception at the end of this + // function. this means that the caller is still notified of errors via the + // exception mechanism, but we leave ourselves in the best state possible for + // the case where ScriptNode::load( continueOnError = true ) will ignore the + // exception that we throw. + + bool errors = false; + const char *s = getenv( "GAFFER_REFERENCE_PATHS" ); + IECore::SearchPath sp( s ? s : "" ); + /// \todo Convert SearchPath to deal in `std::filesystem` rather than `boost::filesystem`. + std::filesystem::path path = sp.find( fileName.string() ).string(); + if( !path.empty() ) + { + PlugEdits::LoadingScope loadingScope( m_referenceState->plugEdits ); + errors = script->executeFile( path.string(), this, /* continueOnError = */ true ); + // deregister "childNodesAreReadOnly" metadata, in case it was baked in the exported file + Metadata::deregisterValue( this, g_childNodesAreReadOnlyName ); + } + + // Do a little bit of post processing on everything that was loaded. + + for( size_t i = 0, e = newChildren->size(); i < e; ++i ) + { + if( Plug *plug = runTimeCast( newChildren->member( i ) ) ) + { + // Make the loaded plugs non-dynamic, because we don't want them + // to be serialised in the script the reference is in - the whole + // point is that they are referenced. + /// \todo Plug flags are not working. We need to introduce an + /// alternative mechanism based on querying parent nodes/plugs + /// for serialisation requirements at the point of serialisation. + plug->setFlags( Plug::Dynamic, false ); + + if( + runTimeCast( plug ) || + runTimeCast( plug ) || + runTimeCast( plug ) + ) + { + // Avoid recursion as it makes it impossible to serialise + // the `x/y` children of ramp points. See RampPlugSerialiser + // for further details of ramp serialisation. + continue; + } + + for( Plug::RecursiveIterator it( plug ); !it.done(); ++it ) + { + (*it)->setFlags( Plug::Dynamic, false ); + } + } + } + + // Transfer connections, values and metadata from the old plugs onto the corresponding new ones. + + for( std::map::const_iterator it = previousPlugs.begin(), eIt = previousPlugs.end(); it != eIt; ++it ) + { + Plug *oldPlug = it->second; + Plug *newPlug = descendant( it->first ); + if( newPlug ) + { + try + { + m_referenceState->plugEdits.transferEdits( oldPlug, newPlug ); + if( newPlug->direction() == Plug::In && oldPlug->direction() == Plug::In ) + { + copyInputsAndValues( oldPlug, newPlug, /* ignoreDefaultValues = */ true ); + } + transferOutputs( oldPlug, newPlug ); + } + catch( const std::exception &e ) + { + msg( + Msg::Warning, + fmt::format( "Loading \"{}\" onto \"{}\"", fileName.generic_string(), getName().c_str() ), + e.what() + ); + } + + } + + // remove the old plug now we're done with it. + oldPlug->parent()->removeChild( oldPlug ); + } + + // Finish up. + + m_referenceState->fileName = fileName; + referenceChangedSignal()( this ); + + if( errors ) + { + throw Exception( fmt::format( "Error loading reference \"{}\"", fileName.generic_string() ) ); + } + +} + +bool SubGraph::hasMetadataEdit( const Plug *plug, const IECore::InternedString key ) const +{ + return m_referenceState && m_referenceState->plugEdits.hasMetadataEdit( plug, key ); +} + +bool SubGraph::isChildEdit( const Plug *plug ) const +{ + return m_referenceState && m_referenceState->plugEdits.isChildEdit( plug ); +} + +bool SubGraph::isReferencePlug( const Plug *plug ) const +{ + // If a plug is the descendant of a plug starting with + // __, and that plug is a direct child of the reference, + // assume that it is for gaffer's internal use, so would + // never come directly from a reference. This lines up + // with the export code in Box::exportForReference(), where + // such plugs are excluded from the export. + + // find ancestor of p which is a direct child of this node: + const Plug* ancestorPlug = plug; + const GraphComponent* parent = plug->parent(); + while( parent != this ) + { + ancestorPlug = runTimeCast< const Plug >( parent ); + if( !ancestorPlug ) + { + // Looks like the plug we're looking for doesn't exist, + // so we exit the loop. + break; + } + parent = ancestorPlug->parent(); + } + + if( ancestorPlug && boost::starts_with( ancestorPlug->getName().c_str(), "__" ) ) + { + return false; + } + + // we know this doesn't come from a reference, + // because it's made during construction. + if( plug == userPlug() ) + { + return false; + } + + // User plugs are not meant to be referenced either. But old + // versions of Gaffer did export them so we must be careful. + // Since we make loaded plugs non-dynamic, we can assume that + // if the plug is dynamic it was added locally by a user + // rather than loaded from a reference. + if( ancestorPlug == userPlug() && plug->getFlags( Plug::Dynamic ) ) + { + return false; + } + // everything else must be from a reference then. + return true; } diff --git a/src/GafferModule/SubGraphBinding.cpp b/src/GafferModule/SubGraphBinding.cpp index f4f06b0b626..e7c8ceb2503 100644 --- a/src/GafferModule/SubGraphBinding.cpp +++ b/src/GafferModule/SubGraphBinding.cpp @@ -259,6 +259,21 @@ void registerProcessor( const std::string &name, object creator ) namespace { +struct ReferenceChangedSlotCaller +{ + void operator()( boost::python::object slot, SubGraphPtr subGraph ) + { + try + { + slot( subGraph ); + } + catch( const error_already_set & ) + { + IECorePython::ExceptionAlgo::translatePythonException(); + } + } +}; + struct ReferenceLoadedSlotCaller { void operator()( boost::python::object slot, ReferencePtr r ) @@ -292,6 +307,12 @@ class ReferenceSerialiser : public NodeSerialiser }; +void loadReferenceWrapper( SubGraph &subGraph, const std::filesystem::path &fileName ) +{ + IECorePython::ScopedGILRelease gilRelease; + subGraph.loadReference( fileName ); +} + void load( Reference &r, const std::filesystem::path &f ) { IECorePython::ScopedGILRelease gilRelease; @@ -303,7 +324,15 @@ void load( Reference &r, const std::filesystem::path &f ) void GafferModule::bindSubGraph() { using SubGraphWrapper = DependencyNodeWrapper; - DependencyNodeClass(); + DependencyNodeClass() + .def( "loadReference", &loadReferenceWrapper ) + .def( "referenceFileName", &SubGraph::referenceFileName, return_value_policy() ) + .def( "referenceChangedSignal", &SubGraph::referenceChangedSignal, return_internal_reference<1>() ) + .def( "hasMetadataEdit", &SubGraph::hasMetadataEdit ) + .def( "isChildEdit", &SubGraph::isChildEdit ) + ; + + SignalClass, ReferenceChangedSlotCaller >( "ReferenceChangedSignal" ); using BoxWrapper = DependencyNodeWrapper; @@ -337,8 +366,6 @@ void GafferModule::bindSubGraph() .def( "load", &load ) .def( "fileName", &Reference::fileName, return_value_policy() ) .def( "referenceLoadedSignal", &Reference::referenceLoadedSignal, return_internal_reference<1>() ) - .def( "hasMetadataEdit", &Reference::hasMetadataEdit ) - .def( "isChildEdit", &Reference::isChildEdit ) ; SignalClass, ReferenceLoadedSlotCaller >( "ReferenceLoadedSignal" ); From 19ddad96cbcc1c5ec9dcb39eb1d78c749f7672af Mon Sep 17 00:00:00 2001 From: John Haddon Date: Thu, 16 Apr 2026 16:05:37 +0100 Subject: [PATCH 4/7] SubGraph : Adopt export functionality from Box --- include/Gaffer/Box.h | 3 +- include/Gaffer/SubGraph.h | 2 ++ src/Gaffer/Box.cpp | 38 +------------------------ src/Gaffer/SubGraph.cpp | 42 +++++++++++++++++++++++++++- src/GafferModule/SubGraphBinding.cpp | 1 + 5 files changed, 46 insertions(+), 40 deletions(-) diff --git a/include/Gaffer/Box.h b/include/Gaffer/Box.h index f67c0937d66..c04cf6fa71c 100644 --- a/include/Gaffer/Box.h +++ b/include/Gaffer/Box.h @@ -58,8 +58,7 @@ class GAFFER_API Box : public SubGraph GAFFER_NODE_DECLARE_TYPE( Gaffer::Box, BoxTypeId, SubGraph ); - /// Exports the contents of the Box so that it can be referenced - /// by a Reference node. + /// \deprecated Use `SubGraph::exportReference()` instead. void exportForReference( const std::filesystem::path &fileName ) const; /// Creates a Box by containing a set of child nodes which diff --git a/include/Gaffer/SubGraph.h b/include/Gaffer/SubGraph.h index 89b1e4db707..82c10599984 100644 --- a/include/Gaffer/SubGraph.h +++ b/include/Gaffer/SubGraph.h @@ -68,6 +68,8 @@ class GAFFER_API SubGraph : public DependencyNode /// to be published and then referenced into multiple `.gfr` files. When /// referencing, the child nodes are not user-editable. + /// Exports the internal node graph as a `.grf` file, ready for referencing. + void exportReference( const std::filesystem::path &fileName ) const; /// Loads a previously exported `.grf` file, replacing the internal node graph. void loadReference( const std::filesystem::path &fileName ); diff --git a/src/Gaffer/Box.cpp b/src/Gaffer/Box.cpp index f8e0f9cc8cb..1908a771d29 100644 --- a/src/Gaffer/Box.cpp +++ b/src/Gaffer/Box.cpp @@ -43,8 +43,6 @@ #include "Gaffer/ScriptNode.h" #include "Gaffer/StandardSet.h" -#include "boost/regex.hpp" - #include "fmt/format.h" #include @@ -104,41 +102,7 @@ Box::~Box() void Box::exportForReference( const std::filesystem::path &fileName ) const { - const ScriptNode *script = scriptNode(); - if( !script ) - { - throw IECore::Exception( "Box::exportForReference called without ScriptNode" ); - } - - // we only want to save out our child nodes and plugs that are visible in the UI, so we build a filter - // to specify just the things to export. - - boost::regex invisiblePlug( "^__.*$" ); - StandardSetPtr toExport = new StandardSet; - for( ChildIterator it = children().begin(), eIt = children().end(); it != eIt; ++it ) - { - if( (*it)->isInstanceOf( Node::staticTypeId() ) ) - { - toExport->add( *it ); - } - else if( const Plug *plug = IECore::runTimeCast( it->get() ) ) - { - if( - !boost::regex_match( plug->getName().c_str(), invisiblePlug ) - && plug != userPlug() - ) - { - toExport->add( *it ); - } - } - } - - ContextPtr context = new Context; - context->set( "valuePlugSerialiser:omitParentNodePlugValues", true ); - context->set( "serialiser:includeParentMetadata", true ); - Context::Scope scopedContext( context.get() ); - - script->serialiseToFile( fileName, this, toExport.get() ); + SubGraph::exportReference( fileName ); } BoxPtr Box::create( Node *parent, const Set *childNodes ) diff --git a/src/Gaffer/SubGraph.cpp b/src/Gaffer/SubGraph.cpp index 378a9ac36ef..c9024280e7a 100644 --- a/src/Gaffer/SubGraph.cpp +++ b/src/Gaffer/SubGraph.cpp @@ -49,6 +49,7 @@ #include "boost/algorithm/string/predicate.hpp" #include "boost/bind.hpp" +#include "boost/regex.hpp" using namespace IECore; using namespace Gaffer; @@ -518,6 +519,45 @@ SubGraph::~SubGraph() { } +void SubGraph::exportReference( const std::filesystem::path &fileName ) const +{ + const ScriptNode *script = scriptNode(); + if( !script ) + { + throw IECore::Exception( "SubGraph::exportForReference called without ScriptNode" ); + } + + // we only want to save out our child nodes and plugs that are visible in the UI, so we build a filter + // to specify just the things to export. + + boost::regex invisiblePlug( "^__.*$" ); + StandardSetPtr toExport = new StandardSet; + for( ChildIterator it = children().begin(), eIt = children().end(); it != eIt; ++it ) + { + if( (*it)->isInstanceOf( Node::staticTypeId() ) ) + { + toExport->add( *it ); + } + else if( const Plug *plug = IECore::runTimeCast( it->get() ) ) + { + if( + !boost::regex_match( plug->getName().c_str(), invisiblePlug ) + && plug != userPlug() + ) + { + toExport->add( *it ); + } + } + } + + ContextPtr context = new Context; + context->set( "valuePlugSerialiser:omitParentNodePlugValues", true ); + context->set( "serialiser:includeParentMetadata", true ); + Context::Scope scopedContext( context.get() ); + + script->serialiseToFile( fileName, this, toExport.get() ); +} + void SubGraph::loadReference( const std::filesystem::path &fileName ) { const char *s = getenv( "GAFFER_REFERENCE_PATHS" ); @@ -821,7 +861,7 @@ bool SubGraph::isReferencePlug( const Plug *plug ) const // __, and that plug is a direct child of the reference, // assume that it is for gaffer's internal use, so would // never come directly from a reference. This lines up - // with the export code in Box::exportForReference(), where + // with the export code in `exportReference()`, where // such plugs are excluded from the export. // find ancestor of p which is a direct child of this node: diff --git a/src/GafferModule/SubGraphBinding.cpp b/src/GafferModule/SubGraphBinding.cpp index e7c8ceb2503..7919271d18f 100644 --- a/src/GafferModule/SubGraphBinding.cpp +++ b/src/GafferModule/SubGraphBinding.cpp @@ -325,6 +325,7 @@ void GafferModule::bindSubGraph() { using SubGraphWrapper = DependencyNodeWrapper; DependencyNodeClass() + .def( "exportReference", &Box::exportReference ) .def( "loadReference", &loadReferenceWrapper ) .def( "referenceFileName", &SubGraph::referenceFileName, return_value_policy() ) .def( "referenceChangedSignal", &SubGraph::referenceChangedSignal, return_internal_reference<1>() ) From e1bdcf8099d64cdb775d4b3a1d3c721d7d055260 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Thu, 16 Apr 2026 21:18:40 +0100 Subject: [PATCH 5/7] SubGraph : Adopt serialisation special cases from Reference The new additions to SubGraphTest are all copied from ReferenceTest, but adapted to use Box nodes for both exporting and referencing. This gives us test coverage for a time when Reference no longer exists. --- include/Gaffer/SubGraph.h | 3 + python/Gaffer/__init__.py | 3 - python/GafferTest/SubGraphTest.py | 1898 +++++++++++++++++++++++ src/Gaffer/SubGraph.cpp | 17 +- src/GafferBindings/MetadataBinding.cpp | 8 +- src/GafferBindings/PlugBinding.cpp | 8 +- src/GafferModule/SpreadsheetBinding.cpp | 12 +- src/GafferModule/SubGraphBinding.cpp | 112 +- 8 files changed, 1993 insertions(+), 68 deletions(-) diff --git a/include/Gaffer/SubGraph.h b/include/Gaffer/SubGraph.h index 82c10599984..2f8c0ad9ad4 100644 --- a/include/Gaffer/SubGraph.h +++ b/include/Gaffer/SubGraph.h @@ -73,6 +73,9 @@ class GAFFER_API SubGraph : public DependencyNode /// Loads a previously exported `.grf` file, replacing the internal node graph. void loadReference( const std::filesystem::path &fileName ); + /// Returns true if this node is referencing a `.grf` file, and false if + /// the child nodes are local. + bool isReference() const; /// Returns the referenced file. If `isReference()` is false, returns an /// empty path. const std::filesystem::path &referenceFileName() const; diff --git a/python/Gaffer/__init__.py b/python/Gaffer/__init__.py index f4c9e884ce8..1e5b76e6f98 100644 --- a/python/Gaffer/__init__.py +++ b/python/Gaffer/__init__.py @@ -60,9 +60,6 @@ from . import NodeAlgo from . import ExtensionAlgo -# Class-level non-UI metadata registration -Metadata.registerValue( Reference, "childNodesAreReadOnly", True ) - def rootPath() : return pathlib.Path( os.path.expandvars( "$GAFFER_ROOT" ) ) diff --git a/python/GafferTest/SubGraphTest.py b/python/GafferTest/SubGraphTest.py index bec1036d6da..7b44346abf4 100644 --- a/python/GafferTest/SubGraphTest.py +++ b/python/GafferTest/SubGraphTest.py @@ -34,8 +34,13 @@ # ########################################################################## +import collections +import os +import pathlib import unittest +import imath + import IECore import Gaffer @@ -138,5 +143,1898 @@ def testCorrespondingInputWithBoxOutAndDots( self ) : self.assertEqual( b.correspondingInput( b["o"].promotedPlug() ), b["i"].promotedPlug() ) + def testLoad( self ) : + + s = Gaffer.ScriptNode() + + s["n1"] = GafferTest.AddNode() + s["n2"] = GafferTest.AddNode() + s["n2"]["op1"].setInput( s["n1"]["sum"] ) + + b = Gaffer.Box.create( s, Gaffer.StandardSet( [ s["n1"] ] ) ) + + b.exportReference( self.temporaryDirectory() / "test.grf" ) + + s["r"] = Gaffer.Box() + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertTrue( "n1" in s["r"] ) + self.assertTrue( s["r"]["sum"].getInput().isSame( s["r"]["n1"]["sum"] ) ) + + def testSerialisation( self ) : + + s = Gaffer.ScriptNode() + + s["n1"] = GafferTest.AddNode() + s["n2"] = GafferTest.AddNode() + s["n2"]["op1"].setInput( s["n1"]["sum"] ) + b = Gaffer.Box.create( s, Gaffer.StandardSet( [ s["n1"] ] ) ) + Gaffer.PlugAlgo.promote( b["n1"]["op1"] ) + + b.exportReference( self.temporaryDirectory() / "test.grf" ) + + s = Gaffer.ScriptNode() + s["r"] = Gaffer.Box() + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertTrue( "n1" in s["r"] ) + self.assertTrue( s["r"]["n1"]["op1"].getInput().isSame( s["r"]["op1"] ) ) + self.assertTrue( s["r"]["sum"].getInput().isSame( s["r"]["n1"]["sum"] ) ) + + s["r"]["op1"].setValue( 25 ) + self.assertEqual( s["r"]["sum"].getValue(), 25 ) + + ss = s.serialise() + + # referenced nodes should be referenced only, and not + # explicitly mentioned in the serialisation at all. + self.assertTrue( "AddNode" not in ss ) + # but the values of user plugs should be stored, so + # they can override the values from the reference. + self.assertTrue( "\"op1\"" in ss ) + + s2 = Gaffer.ScriptNode() + s2.execute( ss ) + + self.assertTrue( "n1" in s2["r"] ) + self.assertTrue( s2["r"]["sum"].getInput().isSame( s2["r"]["n1"]["sum"] ) ) + self.assertEqual( s2["r"]["sum"].getValue(), 25 ) + + def testReload( self ) : + + s = Gaffer.ScriptNode() + + s["n1"] = GafferTest.AddNode() + s["n2"] = GafferTest.AddNode() + s["n3"] = GafferTest.AddNode() + s["n2"]["op1"].setInput( s["n1"]["sum"] ) + s["n3"]["op1"].setInput( s["n2"]["sum"] ) + b = Gaffer.Box.create( s, Gaffer.StandardSet( [ s["n2"] ] ) ) + Gaffer.PlugAlgo.promote( b["n2"]["op2"] ) + + b.exportReference( self.temporaryDirectory() / "test.grf" ) + + s2 = Gaffer.ScriptNode() + s2["n1"] = GafferTest.AddNode() + s2["n3"] = GafferTest.AddNode() + s2["n4"] = GafferTest.AddNode() + s2["r"] = Gaffer.Box() + s2["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + s2["r"]["op1"].setInput( s2["n1"]["sum"] ) + s2["r"]["op2"].setValue( 1001 ) + s2["n3"]["op1"].setInput( s2["r"]["sum"] ) + s2["n4"]["op1"].setInput( s2["r"]["op2"] ) + + self.assertTrue( "n2" in s2["r"] ) + self.assertTrue( s2["r"]["n2"]["op1"].getInput().isSame( s2["r"]["op1"] ) ) + self.assertTrue( s2["r"]["n2"]["op2"].getInput().isSame( s2["r"]["op2"] ) ) + self.assertEqual( s2["r"]["op2"].getValue(), 1001 ) + self.assertTrue( s2["r"]["sum"].getInput().isSame( s2["r"]["n2"]["sum"] ) ) + self.assertTrue( s2["r"]["op1"].getInput().isSame( s2["n1"]["sum"] ) ) + self.assertTrue( s2["n3"]["op1"].getInput().isSame( s2["r"]["sum"] ) ) + self.assertTrue( s2["n4"]["op1"].getInput() and s2["n4"]["op1"].getInput().isSame( s2["r"]["op2"] ) ) + originalReferencedNames = s2["r"].keys() + + b["anotherNode"] = GafferTest.AddNode() + Gaffer.PlugAlgo.promote( b["anotherNode"]["op2"] ) + s.serialiseToFile( self.temporaryDirectory() / "test.grf", b ) + + s2["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertTrue( "n2" in s2["r"] ) + self.assertEqual( set( s2["r"].keys() ), set( originalReferencedNames + [ "anotherNode", "op3" ] ) ) + self.assertTrue( s2["r"]["n2"]["op1"].getInput().isSame( s2["r"]["op1"] ) ) + self.assertTrue( s2["r"]["n2"]["op2"].getInput().isSame( s2["r"]["op2"] ) ) + self.assertEqual( s2["r"]["op2"].getValue(), 1001 ) + self.assertTrue( s2["r"]["anotherNode"]["op2"].getInput().isSame( s2["r"]["op3"] ) ) + self.assertTrue( s2["r"]["sum"].getInput().isSame( s2["r"]["n2"]["sum"] ) ) + self.assertTrue( s2["r"]["op1"].getInput().isSame( s2["n1"]["sum"] ) ) + self.assertTrue( s2["n3"]["op1"].getInput().isSame( s2["r"]["sum"] ) ) + self.assertTrue( s2["n4"]["op1"].getInput() and s2["n4"]["op1"].getInput().isSame( s2["r"]["op2"] ) ) + + def testReloadDoesntRemoveCustomPlugs( self ) : + + # plugs unrelated to referencing shouldn't disappear when a reference is + # reloaded. various parts of the ui might be using them for other purposes. + + s = Gaffer.ScriptNode() + + s["n1"] = GafferTest.AddNode() + s["n2"] = GafferTest.AddNode() + s["n2"]["op1"].setInput( s["n1"]["sum"] ) + + b = Gaffer.Box.create( s, Gaffer.StandardSet( [ s["n1"] ] ) ) + b.exportReference( self.temporaryDirectory() / "test.grf" ) + + s2 = Gaffer.ScriptNode() + s2["r"] = Gaffer.Box() + s2["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + s2["r"]["__mySpecialPlug"] = Gaffer.IntPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + + s2["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertTrue( "__mySpecialPlug" in s2["r"] ) + + def testLoadScriptWithReference( self ) : + + s = Gaffer.ScriptNode() + s["n1"] = GafferTest.AddNode() + s["n2"] = GafferTest.AddNode() + s["n3"] = GafferTest.AddNode() + s["n2"]["op1"].setInput( s["n1"]["sum"] ) + s["n3"]["op1"].setInput( s["n2"]["sum"] ) + + b = Gaffer.Box.create( s, Gaffer.StandardSet( [ s["n2"] ] ) ) + Gaffer.PlugAlgo.promote( b["n2"]["op2"] ) + b.exportReference( self.temporaryDirectory() / "test.grf" ) + + s2 = Gaffer.ScriptNode() + s2["r"] = Gaffer.Box() + s2["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + s2["a"] = GafferTest.AddNode() + + s2["r"]["op2"].setValue( 123 ) + s2["r"]["op1"].setInput( s2["a"]["sum"] ) + + self.assertTrue( "n2" in s2["r"] ) + self.assertTrue( "sum" in s2["r"] ) + self.assertTrue( s2["r"]["op1"].getInput().isSame( s2["a"]["sum"] ) ) + + s2["fileName"].setValue( self.temporaryDirectory() / "test.gfr" ) + s2.save() + + s3 = Gaffer.ScriptNode() + s3["fileName"].setValue( self.temporaryDirectory() / "test.gfr" ) + s3.load() + + self.assertEqual( s3["r"].keys(), s2["r"].keys() ) + self.assertEqual( s3["r"]["user"].keys(), s2["r"]["user"].keys() ) + self.assertEqual( s3["r"]["op2"].getValue(), 123 ) + self.assertTrue( s3["r"]["op1"].getInput().isSame( s3["a"]["sum"] ) ) + + def testReferenceExportCustomPlugsFromBoxes( self ) : + + s = Gaffer.ScriptNode() + s["n1"] = GafferTest.AddNode() + + b = Gaffer.Box.create( s, Gaffer.StandardSet( [ s["n1"] ] ) ) + b["myCustomPlug"] = Gaffer.IntPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + b["__invisiblePlugThatShouldntGetExported"] = Gaffer.IntPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + + b.exportReference( self.temporaryDirectory() / "test.grf" ) + + s2 = Gaffer.ScriptNode() + s2["r"] = Gaffer.Box() + s2["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertTrue( "myCustomPlug" in s2["r"] ) + self.assertTrue( "__invisiblePlugThatShouldntGetExported" not in s2["r"] ) + + def testPlugMetadata( self ) : + + s = Gaffer.ScriptNode() + s["n1"] = GafferTest.AddNode() + + b = Gaffer.Box.create( s, Gaffer.StandardSet( [ s["n1"] ] ) ) + p = Gaffer.PlugAlgo.promote( b["n1"]["op1"] ) + + Gaffer.Metadata.registerValue( p, "description", "ppp" ) + + b.exportReference( self.temporaryDirectory() / "test.grf" ) + + s2 = Gaffer.ScriptNode() + s2["r"] = Gaffer.Box() + s2["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertEqual( Gaffer.Metadata.value( s2["r"].descendant( p.relativeName( b ) ), "description" ), "ppp" ) + + s3 = Gaffer.ScriptNode() + s3.execute( s2.serialise() ) + self.assertEqual( Gaffer.Metadata.value( s3["r"].descendant( p.relativeName( b ) ), "description" ), "ppp" ) + + def testMetadataIsntResaved( self ) : + + s = Gaffer.ScriptNode() + s["n1"] = GafferTest.AddNode() + + b = Gaffer.Box.create( s, Gaffer.StandardSet( [ s["n1"] ] ) ) + p = Gaffer.PlugAlgo.promote( b["n1"]["op1"] ) + + Gaffer.Metadata.registerValue( p, "description", "ppp" ) + + b.exportReference( self.temporaryDirectory() / "test.grf" ) + + s2 = Gaffer.ScriptNode() + s2["r"] = Gaffer.Box() + s2["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertTrue( "description" not in s2.serialise() ) + + def testSinglePlugWithMetadata( self ) : + + s = Gaffer.ScriptNode() + s["b"] = Gaffer.Box() + s["b"]["p"] = Gaffer.Plug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + + Gaffer.Metadata.registerValue( s["b"]["p"], "description", "ddd" ) + + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + s["r"] = Gaffer.Box() + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertEqual( Gaffer.Metadata.value( s["r"]["p"], "description" ), "ddd" ) + + def testEditPlugMetadata( self ) : + + # Export a box with some metadata + + s = Gaffer.ScriptNode() + s["n1"] = GafferTest.AddNode() + + b = Gaffer.Box.create( s, Gaffer.StandardSet( [ s["n1"] ] ) ) + p = Gaffer.PlugAlgo.promote( b["n1"]["op1"] ) + p.setName( "p" ) + + Gaffer.Metadata.registerValue( p, "test", "referenced" ) + + b.exportReference( self.temporaryDirectory() / "test.grf" ) + + # Reference it, and check it loaded. + + s2 = Gaffer.ScriptNode() + s2["r"] = Gaffer.Box() + s2["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertEqual( Gaffer.Metadata.value( s2["r"]["p"], "test" ), "referenced" ) + + # Edit it, and check it overwrote the original. + + Gaffer.Metadata.registerValue( s2["r"]["p"], "test", "edited" ) + self.assertEqual( Gaffer.Metadata.value( s2["r"]["p"], "test" ), "edited" ) + + # Save and load the script, and check the edit stays in place. + + s3 = Gaffer.ScriptNode() + s3.execute( s2.serialise() ) + self.assertEqual( Gaffer.Metadata.value( s3["r"]["p"], "test" ), "edited" ) + + # Reload the reference, and check the edit stays in place. + + s3["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + self.assertEqual( Gaffer.Metadata.value( s3["r"]["p"], "test" ), "edited" ) + + def testStaticMetadataRegistrationIsntAnEdit( self ) : + + # Export a box + + s = Gaffer.ScriptNode() + + s["b"] = Gaffer.Box() + s["b"]["staticMetadataTestPlug"] = Gaffer.IntPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + # Reference it + + s["r"] = Gaffer.Box() + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + # Make a static metadata registration. Although this will + # be signalled as a metadata change, it must not be considered + # to be a metadata edit on the reference, as it does not apply + # to a specific plug instance. + + Gaffer.Metadata.registerValue( Gaffer.Box, "staticMetadataTestPlug", "test", 10 ) + self.assertFalse( s["r"].hasMetadataEdit( s["r"]["staticMetadataTestPlug"], "test" ) ) + + def testAddPlugMetadata( self ) : + + # Export a box with no metadata + + s = Gaffer.ScriptNode() + s["n1"] = GafferTest.AddNode() + + b = Gaffer.Box.create( s, Gaffer.StandardSet( [ s["n1"] ] ) ) + p = Gaffer.PlugAlgo.promote( b["n1"]["op1"] ) + p.setName( "p" ) + + b.exportReference( self.temporaryDirectory() / "test.grf" ) + + # Reference it, and check it loaded. + + s2 = Gaffer.ScriptNode() + s2["r"] = Gaffer.Box() + s2["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + # Add some metadata to the Reference node (not the reference file) + + Gaffer.Metadata.registerValue( s2["r"]["p"], "test", "added" ) + self.assertEqual( Gaffer.Metadata.value( s2["r"]["p"], "test" ), "added" ) + + # Save and load the script, and check the added metadata stays in place. + + s3 = Gaffer.ScriptNode() + s3.execute( s2.serialise() ) + self.assertEqual( Gaffer.Metadata.value( s3["r"]["p"], "test" ), "added" ) + + # Reload the reference, and check the edit stays in place. + + s3["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + self.assertEqual( Gaffer.Metadata.value( s3["r"]["p"], "test" ), "added" ) + + def testReloadWithUnconnectedPlugs( self ) : + + s = Gaffer.ScriptNode() + s["b"] = Gaffer.Box() + s["b"]["p"] = Gaffer.Plug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + s["r"] = Gaffer.Box() + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertEqual( s["r"].keys(), [ "user", "p" ] ) + + s2 = Gaffer.ScriptNode() + s2.execute( s.serialise() ) + + self.assertEqual( s2["r"].keys(), [ "user", "p" ] ) + + def testReloadRefreshesMetadata( self ) : + + s = Gaffer.ScriptNode() + s["b"] = Gaffer.Box() + s["b"]["p"] = Gaffer.Plug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + s["r"] = Gaffer.Box() + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertEqual( Gaffer.Metadata.value( s["r"]["p"], "test" ), None ) + + Gaffer.Metadata.registerValue( s["b"]["p"], "test", 10 ) + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertEqual( Gaffer.Metadata.value( s["r"]["p"], "test" ), 10 ) + + def testLoadThrowsExceptionsOnError( self ) : + + s = Gaffer.ScriptNode() + s["b"] = Gaffer.Box() + s["b"]["n"] = GafferTest.StringInOutNode() + + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + self.addCleanup( setattr, GafferTest, "StringInOutNode", GafferTest.StringInOutNode ) + del GafferTest.StringInOutNode # induce a failure during loading + + s2 = Gaffer.ScriptNode() + s2["r"] = Gaffer.Box() + + with IECore.CapturingMessageHandler() as mh : + self.assertRaises( Exception, s2["r"].loadReference, self.temporaryDirectory() / "test.grf" ) + + self.assertEqual( len( mh.messages ), 2 ) + self.assertTrue( "has no attribute 'StringInOutNode'" in mh.messages[0].message ) + self.assertTrue( "KeyError: 'n'" in mh.messages[1].message ) + + def testErrorTolerantLoading( self ) : + + # make a box containing 2 nodes, and export it. + + s = Gaffer.ScriptNode() + s["b"] = Gaffer.Box() + s["b"]["s"] = GafferTest.StringInOutNode() + s["b"]["a"] = GafferTest.AddNode() + + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + # import it into a script. + + s2 = Gaffer.ScriptNode() + s2["r"] = Gaffer.Box() + s2["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertTrue( "a" in s2["r"] ) + self.assertTrue( isinstance( s2["r"]["a"], GafferTest.AddNode ) ) + + # save that script, and then mysteriously + # disable GafferTest.StringInOutNode. + + s2["fileName"].setValue( self.temporaryDirectory() / "test.gfr" ) + s2.save() + + self.addCleanup( setattr, GafferTest, "StringInOutNode", GafferTest.StringInOutNode ) + del GafferTest.StringInOutNode + + # load the script, and check that we could at least + # load in the other referenced node. + + s3 = Gaffer.ScriptNode() + s3["fileName"].setValue( self.temporaryDirectory() / "test.gfr" ) + with IECore.CapturingMessageHandler() as mh : + s3.load( continueOnError=True ) + + self.assertTrue( len( mh.messages ) ) + + self.assertTrue( "a" in s3["r"] ) + self.assertTrue( isinstance( s3["r"]["a"], GafferTest.AddNode ) ) + + def testDependencyNode( self ) : + + s = Gaffer.ScriptNode() + + # Make a reference, and check it's a DependencyNode + + s["r"] = Gaffer.Box() + self.assertTrue( isinstance( s["r"], Gaffer.DependencyNode ) ) + self.assertTrue( s["r"].isInstanceOf( Gaffer.DependencyNode.staticTypeId() ) ) + self.assertTrue( isinstance( s["r"], Gaffer.SubGraph ) ) + self.assertTrue( s["r"].isInstanceOf( Gaffer.SubGraph.staticTypeId() ) ) + + # create a box with a promoted output: + s["b"] = Gaffer.Box() + s["b"]["n"] = GafferTest.AddNode() + Gaffer.PlugAlgo.promote( s["b"]["n"]["sum"] ) + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + # load onto reference: + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + self.assertEqual( s["r"].correspondingInput( s["r"]["sum"] ), None ) + self.assertEqual( s["r"].enabledPlug(), None ) + + # Wire it up to support enabledPlug() and correspondingInput() + Gaffer.PlugAlgo.promote( s["b"]["n"]["op1"] ) + s["b"]["n"]["op2"].setValue( 10 ) + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + # reload reference and test: + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + self.assertEqual( s["r"].correspondingInput( s["r"]["sum"] ), None ) + self.assertEqual( s["r"].enabledPlug(), None ) + + # add an enabled plug: + s["b"]["enabled"] = Gaffer.BoolPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + # reload reference and test that's now visible via enabledPlug(): + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + self.assertEqual( s["r"].correspondingInput( s["r"]["sum"] ), None ) + self.assertTrue( s["r"].enabledPlug().isSame( s["r"]["enabled"] ) ) + + # hook up the enabled plug inside the box: + s["b"]["n"]["enabled"].setInput( s["b"]["enabled"] ) + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + # reload reference and test that's now visible via enabledPlug(): + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + self.assertTrue( s["r"].enabledPlug().isSame( s["r"]["enabled"] ) ) + self.assertTrue( s["r"].correspondingInput( s["r"]["sum"] ).isSame( s["r"]["op1"] ) ) + + # Connect it into a network, delete it, and check that we get nice auto-reconnect behaviour + s["a"] = GafferTest.AddNode() + s["r"]["op1"].setInput( s["a"]["sum"] ) + + s["c"] = GafferTest.AddNode() + s["c"]["op1"].setInput( s["r"]["sum"] ) + + s.deleteNodes( filter = Gaffer.StandardSet( [ s["r"] ] ) ) + + self.assertTrue( s["c"]["op1"].getInput().isSame( s["a"]["sum"] ) ) + + def testPlugFlagsOnReload( self ): + + s = Gaffer.ScriptNode() + s["b"] = Gaffer.Box() + s["b"]["s"] = GafferTest.StringInOutNode() + s["b"]["a"] = GafferTest.AddNode() + + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + # import it into a script. + + s2 = Gaffer.ScriptNode() + s2["r"] = Gaffer.Box() + s2["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + s2["r"]["__pluggy"] = Gaffer.Plug( flags = Gaffer.Plug.Flags.Dynamic | Gaffer.Plug.Flags.Default ) + s2["r"]["__pluggy"]["int"] = Gaffer.IntPlug( flags = Gaffer.Plug.Flags.Dynamic | Gaffer.Plug.Flags.Default ) + s2["r"]["__pluggy"]["compound"] = Gaffer.Plug( flags = Gaffer.Plug.Flags.Dynamic | Gaffer.Plug.Flags.Default ) + s2["r"]["__pluggy"]["compound"]["int"] = Gaffer.IntPlug( flags = Gaffer.Plug.Flags.Dynamic | Gaffer.Plug.Flags.Default ) + + self.assertEqual( s2["r"]["__pluggy"].getFlags(), Gaffer.Plug.Flags.Dynamic | Gaffer.Plug.Flags.Default ) + self.assertEqual( s2["r"]["__pluggy"]["int"].getFlags(), Gaffer.Plug.Flags.Dynamic | Gaffer.Plug.Flags.Default ) + self.assertEqual( s2["r"]["__pluggy"]["compound"].getFlags(), Gaffer.Plug.Flags.Dynamic | Gaffer.Plug.Flags.Default ) + self.assertEqual( s2["r"]["__pluggy"]["compound"]["int"].getFlags(), Gaffer.Plug.Flags.Dynamic | Gaffer.Plug.Flags.Default ) + + s2["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertEqual( s2["r"]["__pluggy"].getFlags(), Gaffer.Plug.Flags.Dynamic | Gaffer.Plug.Flags.Default ) + self.assertEqual( s2["r"]["__pluggy"]["int"].getFlags(), Gaffer.Plug.Flags.Dynamic | Gaffer.Plug.Flags.Default ) + self.assertEqual( s2["r"]["__pluggy"]["compound"].getFlags(), Gaffer.Plug.Flags.Dynamic | Gaffer.Plug.Flags.Default ) + self.assertEqual( s2["r"]["__pluggy"]["compound"]["int"].getFlags(), Gaffer.Plug.Flags.Dynamic | Gaffer.Plug.Flags.Default ) + + def testDefaultValues( self ) : + + s = Gaffer.ScriptNode() + s["b"] = Gaffer.Box() + s["b"]["p"] = Gaffer.IntPlug( defaultValue = 1, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["b"]["p"].setValue( 2 ) + s["b"]["c"] = Gaffer.Color3fPlug( defaultValue = imath.Color3f( 1 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["b"]["c"].setValue( imath.Color3f( 0.5 ) ) + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + s["r"] = Gaffer.Box() + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + # The value at the time of box export should be ignored, + # and the box itself should not be modified by the export + # process. + + self.assertEqual( s["r"]["p"].getValue(), 1 ) + self.assertEqual( s["r"]["p"].defaultValue(), 1 ) + self.assertEqual( s["b"]["p"].defaultValue(), 1 ) + self.assertEqual( s["b"]["p"].getValue(), 2 ) + + self.assertEqual( s["r"]["c"].getValue(), imath.Color3f( 1 ) ) + self.assertEqual( s["r"]["c"].defaultValue(), imath.Color3f( 1 ) ) + self.assertEqual( s["b"]["c"].defaultValue(), imath.Color3f( 1 ) ) + self.assertEqual( s["b"]["c"].getValue(), imath.Color3f( 0.5 ) ) + + # And we should be able to save and reload the script + # and have that still be the case. + + s["fileName"].setValue( self.temporaryDirectory() / "test.gfr" ) + s.save() + s.load() + + self.assertEqual( s["r"]["p"].getValue(), 1 ) + self.assertEqual( s["r"]["p"].defaultValue(), 1 ) + self.assertEqual( s["b"]["p"].getValue(), 2 ) + self.assertEqual( s["b"]["p"].defaultValue(), 1 ) + + self.assertEqual( s["r"]["c"].getValue(), imath.Color3f( 1 ) ) + self.assertEqual( s["r"]["c"].defaultValue(), imath.Color3f( 1 ) ) + self.assertEqual( s["b"]["c"].getValue(), imath.Color3f( 0.5 ) ) + self.assertEqual( s["b"]["c"].defaultValue(), imath.Color3f( 1 ) ) + + # If we change the default value on the box and reexport, + # then the reference should pick up the new default, and + # because no value was authored on the reference, the value + # should be the same as the default too. + + s["b"]["p"].setValue( 3 ) + s["b"]["p"].resetDefault() + s["b"]["c"].setValue( imath.Color3f( 0.25 ) ) + s["b"]["c"].resetDefault() + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertEqual( s["r"]["p"].getValue(), 3 ) + self.assertEqual( s["r"]["p"].defaultValue(), 3 ) + self.assertEqual( s["b"]["p"].getValue(), 3 ) + self.assertEqual( s["b"]["p"].defaultValue(), 3 ) + + self.assertEqual( s["r"]["c"].getValue(), imath.Color3f( 0.25 ) ) + self.assertEqual( s["r"]["c"].defaultValue(), imath.Color3f( 0.25 ) ) + self.assertEqual( s["b"]["c"].getValue(), imath.Color3f( 0.25 ) ) + self.assertEqual( s["b"]["c"].defaultValue(), imath.Color3f( 0.25 ) ) + + # And that should still hold after saving and reloading the script. + + s.save() + s.load() + self.assertEqual( s["r"]["p"].getValue(), 3 ) + self.assertEqual( s["r"]["p"].defaultValue(), 3 ) + self.assertEqual( s["b"]["p"].getValue(), 3 ) + self.assertEqual( s["b"]["p"].defaultValue(), 3 ) + + self.assertEqual( s["r"]["c"].getValue(), imath.Color3f( 0.25 ) ) + self.assertEqual( s["r"]["c"].defaultValue(), imath.Color3f( 0.25 ) ) + self.assertEqual( s["b"]["c"].getValue(), imath.Color3f( 0.25 ) ) + self.assertEqual( s["b"]["c"].defaultValue(), imath.Color3f( 0.25 ) ) + + # But if the user changes the value on the reference node, + # it should be kept. + + s["r"]["p"].setValue( 100 ) + s["r"]["c"].setValue( imath.Color3f( 100 ) ) + + self.assertEqual( s["r"]["p"].getValue(), 100 ) + self.assertEqual( s["r"]["p"].defaultValue(), 3 ) + self.assertEqual( s["b"]["p"].getValue(), 3 ) + self.assertEqual( s["b"]["p"].defaultValue(), 3 ) + + self.assertEqual( s["r"]["c"].getValue(), imath.Color3f( 100 ) ) + self.assertEqual( s["r"]["c"].defaultValue(), imath.Color3f( 0.25 ) ) + self.assertEqual( s["b"]["c"].getValue(), imath.Color3f( 0.25 ) ) + self.assertEqual( s["b"]["c"].defaultValue(), imath.Color3f( 0.25 ) ) + + # And a save and load shouldn't change that. + + s.save() + s.load() + + self.assertEqual( s["r"]["p"].getValue(), 100 ) + self.assertEqual( s["r"]["p"].defaultValue(), 3 ) + self.assertEqual( s["b"]["p"].getValue(), 3 ) + self.assertEqual( s["b"]["p"].defaultValue(), 3 ) + + self.assertEqual( s["r"]["c"].getValue(), imath.Color3f( 100 ) ) + self.assertEqual( s["r"]["c"].defaultValue(), imath.Color3f( 0.25 ) ) + self.assertEqual( s["b"]["c"].getValue(), imath.Color3f( 0.25 ) ) + self.assertEqual( s["b"]["c"].defaultValue(), imath.Color3f( 0.25 ) ) + + # And now the user has changed a value, only the + # default value should be updated if we load a new + # reference. + + s["b"]["p"].setValue( 4 ) + s["b"]["p"].resetDefault() + s["b"]["c"].setValue( imath.Color3f( 4 ) ) + s["b"]["c"].resetDefault() + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertEqual( s["r"]["p"].getValue(), 100 ) + self.assertEqual( s["r"]["p"].defaultValue(), 4 ) + self.assertEqual( s["b"]["p"].getValue(), 4 ) + self.assertEqual( s["b"]["p"].defaultValue(), 4 ) + + self.assertEqual( s["r"]["c"].getValue(), imath.Color3f( 100 ) ) + self.assertEqual( s["r"]["c"].defaultValue(), imath.Color3f( 4 ) ) + self.assertEqual( s["b"]["c"].getValue(), imath.Color3f( 4 ) ) + self.assertEqual( s["b"]["c"].defaultValue(), imath.Color3f( 4 ) ) + + # And a save and load shouldn't change anything. + + s.save() + s.load() + + self.assertEqual( s["r"]["p"].getValue(), 100 ) + self.assertEqual( s["r"]["p"].defaultValue(), 4 ) + self.assertEqual( s["b"]["p"].getValue(), 4 ) + self.assertEqual( s["b"]["p"].defaultValue(), 4 ) + + self.assertEqual( s["r"]["c"].getValue(), imath.Color3f( 100 ) ) + self.assertEqual( s["r"]["c"].defaultValue(), imath.Color3f( 4 ) ) + self.assertEqual( s["b"]["c"].getValue(), imath.Color3f( 4 ) ) + self.assertEqual( s["b"]["c"].defaultValue(), imath.Color3f( 4 ) ) + + # And there shouldn't be a single setValue() call in the exported file. + + e = "".join( open( self.temporaryDirectory() / "test.grf", encoding = "utf-8" ).readlines() ) + self.assertTrue( "setValue" not in e ) + + def testInternalNodeDefaultValues( self ) : + + s = Gaffer.ScriptNode() + s["b"] = Gaffer.Box() + s["b"]["n"] = Gaffer.Node() + s["b"]["n"]["p"] = Gaffer.IntPlug( defaultValue = 1, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["b"]["n"]["p"].setValue( 2 ) + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + s["r"] = Gaffer.Box() + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + # Nothing at all should have changed about the + # values and defaults on the internal nodes. + + self.assertEqual( s["r"]["n"]["p"].getValue(), 2 ) + self.assertEqual( s["r"]["n"]["p"].defaultValue(), 1 ) + + # And we should be able to save and reload the script + # and have that still be the case. + + s["fileName"].setValue( self.temporaryDirectory() / "test.gfr" ) + s.save() + s.load() + + self.assertEqual( s["r"]["n"]["p"].getValue(), 2 ) + self.assertEqual( s["r"]["n"]["p"].defaultValue(), 1 ) + + def testNodeMetadata( self ) : + + s = Gaffer.ScriptNode() + s["b"] = Gaffer.Box() + + Gaffer.Metadata.registerValue( s["b"], "description", "Test description" ) + Gaffer.Metadata.registerValue( s["b"], "nodeGadget:color", imath.Color3f( 1, 0, 0 ) ) + + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + s["r"] = Gaffer.Box() + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertEqual( Gaffer.Metadata.value( s["r"], "description" ), "Test description" ) + self.assertEqual( Gaffer.Metadata.value( s["r"], "nodeGadget:color" ), imath.Color3f( 1, 0, 0 ) ) + + def testVersionMetadata( self ) : + + s = Gaffer.ScriptNode() + s["b"] = Gaffer.Box() + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + s["r"] = Gaffer.Box() + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertEqual( Gaffer.Metadata.value( s["r"], "serialiser:milestoneVersion" ), Gaffer.About.milestoneVersion() ) + self.assertEqual( Gaffer.Metadata.value( s["r"], "serialiser:majorVersion" ), Gaffer.About.majorVersion() ) + self.assertEqual( Gaffer.Metadata.value( s["r"], "serialiser:minorVersion" ), Gaffer.About.minorVersion() ) + self.assertEqual( Gaffer.Metadata.value( s["r"], "serialiser:patchVersion" ), Gaffer.About.patchVersion() ) + + self.assertNotIn( "serialiser:milestoneVersion", Gaffer.Metadata.registeredValues( s["r"], Gaffer.Metadata.RegistrationTypes.InstancePersistent ) ) + self.assertNotIn( "serialiser:majorVersion", Gaffer.Metadata.registeredValues( s["r"], Gaffer.Metadata.RegistrationTypes.InstancePersistent ) ) + self.assertNotIn( "serialiser:minorVersion", Gaffer.Metadata.registeredValues( s["r"], Gaffer.Metadata.RegistrationTypes.InstancePersistent ) ) + self.assertNotIn( "serialiser:patchVersion", Gaffer.Metadata.registeredValues( s["r"], Gaffer.Metadata.RegistrationTypes.InstancePersistent ) ) + + def testSerialiseWithoutLoading( self ) : + + s = Gaffer.ScriptNode() + s["r"] = Gaffer.Box() + + s2 = Gaffer.ScriptNode() + s2.execute( s.serialise() ) + + def testUserPlugMetadata( self ) : + + # People should be able to do what they want with the user plug, + # and anything they do should be serialised appropriately. + + s = Gaffer.ScriptNode() + s["r"] = Gaffer.Box() + s["r"]["user"]["p"] = Gaffer.IntPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + + Gaffer.Metadata.registerValue( s["r"]["user"], "testPersistent", 1, persistent = True ) + Gaffer.Metadata.registerValue( s["r"]["user"], "testNonPersistent", 2, persistent = False ) + + Gaffer.Metadata.registerValue( s["r"]["user"]["p"], "testPersistent", 3, persistent = True ) + Gaffer.Metadata.registerValue( s["r"]["user"]["p"], "testNonPersistent", 4, persistent = False ) + + self.assertEqual( Gaffer.Metadata.value( s["r"]["user"], "testPersistent" ), 1 ) + self.assertEqual( Gaffer.Metadata.value( s["r"]["user"], "testNonPersistent" ), 2 ) + + self.assertEqual( Gaffer.Metadata.value( s["r"]["user"]["p"], "testPersistent" ), 3 ) + self.assertEqual( Gaffer.Metadata.value( s["r"]["user"]["p"], "testNonPersistent" ), 4 ) + + s2 = Gaffer.ScriptNode() + s2.execute( s.serialise() ) + + self.assertEqual( Gaffer.Metadata.value( s2["r"]["user"], "testPersistent" ), 1 ) + self.assertEqual( Gaffer.Metadata.value( s2["r"]["user"], "testNonPersistent" ), None ) + + self.assertEqual( Gaffer.Metadata.value( s2["r"]["user"]["p"], "testPersistent" ), 3 ) + self.assertEqual( Gaffer.Metadata.value( s2["r"]["user"]["p"], "testNonPersistent" ), None ) + + def testNamespaceIsClear( self ) : + + # We need the namespace of the node to be empty, so + # that people can call plugs anything they want when + # authoring references. + + r = Gaffer.Box() + n = Gaffer.Node() + self.assertEqual( r.keys(), n.keys() ) + + def testPlugCalledFileName( self ) : + + s = Gaffer.ScriptNode() + + s["b"] = Gaffer.Box() + s["b"]["fileName"] = Gaffer.StringPlug( defaultValue = "iAmUsingThisForMyOwnPurposes", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + s["r"] = Gaffer.Box() + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertEqual( s["r"]["fileName"].getValue(), "iAmUsingThisForMyOwnPurposes" ) + + def testFileNameAccessor( self ) : + + s = Gaffer.ScriptNode() + s["b"] = Gaffer.Box() + s["b"]["p"] = Gaffer.Plug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + s["r"] = Gaffer.Box() + self.assertEqual( s["r"].referenceFileName(), None ) + + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + self.assertEqual( s["r"].referenceFileName(), self.temporaryDirectory() / "test.grf" ) + + def testUndo( self ) : + + s = Gaffer.ScriptNode() + s["b"] = Gaffer.Box() + s["b"]["p"] = Gaffer.Plug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + s["r"] = Gaffer.Box() + self.assertEqual( s["r"].referenceFileName(), None ) + self.assertTrue( "p" not in s["r"] ) + + State = collections.namedtuple( "State", [ "keys", "fileName" ] ) + states = [] + def referenceChanged( node ) : + states.append( State( keys = node.keys(), fileName = node.referenceFileName() ) ) + + s["r"].referenceChangedSignal().connect( referenceChanged ) + + with Gaffer.UndoScope( s ) : + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertTrue( "p" in s["r"] ) + self.assertEqual( s["r"].referenceFileName(), self.temporaryDirectory() / "test.grf" ) + self.assertTrue( len( states ), 1 ) + self.assertEqual( states[0], State( [ "user", "p" ], self.temporaryDirectory() / "test.grf" ) ) + + s.undo() + self.assertEqual( s["r"].referenceFileName(), None ) + self.assertTrue( "p" not in s["r"] ) + self.assertTrue( len( states ), 2 ) + self.assertEqual( states[1], State( [ "user" ], None ) ) + + s.redo() + self.assertTrue( "p" in s["r"] ) + self.assertEqual( s["r"].referenceFileName(), self.temporaryDirectory() / "test.grf" ) + self.assertTrue( len( states ), 3 ) + self.assertEqual( states[2], State( [ "user", "p" ], self.temporaryDirectory() / "test.grf" ) ) + + def testUserPlugsNotReferenced( self ) : + + s = Gaffer.ScriptNode() + + s["b"] = Gaffer.Box() + s["b"]["user"]["a"] = Gaffer.IntPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + self.assertTrue( "a" in s["b"]["user"] ) + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + s["r"] = Gaffer.Box() + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + self.assertTrue( "a" not in s["r"]["user"] ) + + a = Gaffer.IntPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + b = Gaffer.IntPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["r"]["user"]["a"] = a + s["r"]["user"]["b"] = b + self.assertTrue( s["r"]["user"]["a"].isSame( a ) ) + self.assertTrue( s["r"]["user"]["b"].isSame( b ) ) + + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + self.assertTrue( s["r"]["user"]["a"].isSame( a ) ) + self.assertTrue( s["r"]["user"]["b"].isSame( b ) ) + + s2 = Gaffer.ScriptNode() + s2.execute( s.serialise() ) + + self.assertTrue( "a" in s2["r"]["user"] ) + self.assertTrue( "b" in s2["r"]["user"] ) + + def testCopyPaste( self ) : + + s = Gaffer.ScriptNode() + + s["b"] = Gaffer.Box() + s["b"]["a1"] = GafferTest.AddNode() + s["b"]["a2"] = GafferTest.AddNode() + s["b"]["a2"]["op1"].setInput( s["b"]["a1"]["sum"] ) + Gaffer.PlugAlgo.promote( s["b"]["a1"]["op1"] ) + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + s["r"] = Gaffer.Box() + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + s.execute( s.serialise( parent = s["r"], filter = Gaffer.StandardSet( [ s["r"]["a1"], s["r"]["a2"] ] ) ) ) + + self.assertTrue( "a1" in s ) + self.assertTrue( "a2" in s ) + self.assertTrue( s["a2"]["op1"].getInput().isSame( s["a1"]["sum"] ) ) + + def testReloadWithNestedInputConnections( self ) : + + s = Gaffer.ScriptNode() + + s["b"] = Gaffer.Box() + s["b"]["array"] = Gaffer.ArrayPlug( elementPrototype = Gaffer.IntPlug(), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["b"]["color"] = Gaffer.Color3fPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + s["a"] = GafferTest.AddNode() + + s["r"] = Gaffer.Box() + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + s["r"]["array"][0].setInput( s["a"]["sum"] ) + s["r"]["array"][1].setInput( s["a"]["sum"] ) + s["r"]["color"]["g"].setInput( s["a"]["sum"] ) + + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertTrue( s["r"]["array"][0].getInput().isSame( s["a"]["sum"] ) ) + self.assertTrue( s["r"]["array"][1].getInput().isSame( s["a"]["sum"] ) ) + self.assertTrue( s["r"]["color"]["g"].getInput().isSame( s["a"]["sum"] ) ) + + def testReloadWithNestedOutputConnections( self ) : + + s = Gaffer.ScriptNode() + + s["b"] = Gaffer.Box() + s["b"]["color"] = Gaffer.Color3fPlug( + direction = Gaffer.Plug.Direction.Out, + flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic + ) + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + s["a"] = GafferTest.AddNode() + + s["r"] = Gaffer.Box() + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + s["a"]["op1"].setInput( s["r"]["color"]["g"] ) + + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertTrue( s["a"]["op1"].getInput().isSame( s["r"]["color"]["g"] ) ) + + def testReloadWithBoxIO( self ) : + + s = Gaffer.ScriptNode() + + s["b"] = Gaffer.Box() + s["b"]["a"] = GafferTest.AddNode() + + s["b"]["i"] = Gaffer.BoxIn() + s["b"]["i"]["name"].setValue( "in" ) + s["b"]["i"].setup( s["b"]["a"]["op1"] ) + s["b"]["a"]["op1"].setInput( s["b"]["i"]["out"] ) + + s["b"]["o"] = Gaffer.BoxOut() + s["b"]["o"]["name"].setValue( "out" ) + s["b"]["o"].setup( s["b"]["a"]["sum"] ) + s["b"]["o"]["in"].setInput( s["b"]["a"]["sum"] ) + + referenceFileName = self.temporaryDirectory() / "test.grf" + s["b"].exportReference( referenceFileName ) + + s["a1"] = GafferTest.AddNode() + + s["r"] = Gaffer.Box() + s["r"].loadReference( referenceFileName ) + s["r"]["in"].setInput( s["a1"]["sum"] ) + + s["a2"] = GafferTest.AddNode() + s["a2"]["op1"].setInput( s["r"]["out"] ) + + def assertReferenceConnections() : + + self.assertTrue( s["a2"]["op1"].source().isSame( s["r"]["a"]["sum"] ) ) + self.assertTrue( s["r"]["a"]["op1"].source().isSame( s["a1"]["sum"] ) ) + + self.assertEqual( + set( s["r"].keys() ), + set( [ "in", "out", "user", "a", "i", "o" ] ), + ) + + assertReferenceConnections() + + s["r"].loadReference( referenceFileName ) + + assertReferenceConnections() + + def testSearchPaths( self ) : + + s = Gaffer.ScriptNode() + + referenceFile = pathlib.Path( "test.grf" ) + + boxA = Gaffer.Box( "BoxA" ) + boxA["p"] = Gaffer.StringPlug( defaultValue = "a", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s.addChild( boxA ) + boxPathA = self.temporaryDirectory() / "a" + os.makedirs( boxPathA ) + fileA = boxPathA / referenceFile + boxA.exportReference( fileA ) + + boxB = Gaffer.Box( "BoxB" ) + boxB["p"] = Gaffer.StringPlug( defaultValue = "b", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s.addChild( boxB ) + boxPathB = self.temporaryDirectory() / "b" + os.makedirs( boxPathB ) + fileB = boxPathB / referenceFile + boxB.exportReference( fileB ) + + searchPathA = os.pathsep.join( [boxPathA.as_posix(), boxPathB.as_posix()] ) + searchPathB = os.pathsep.join( [boxPathB.as_posix(), boxPathA.as_posix()] ) + + os.environ["GAFFER_REFERENCE_PATHS"] = searchPathA + s["r"] = Gaffer.Box() + + s["r"].loadReference(referenceFile) + self.assertEqual( s["r"].referenceFileName(), referenceFile ) + self.assertEqual( s["r"]["p"].getValue(), "a" ) + + os.environ["GAFFER_REFERENCE_PATHS"] = searchPathB + s["r"].loadReference( referenceFile ) + self.assertEqual( s["r"].referenceFileName(), referenceFile ) + self.assertEqual( s["r"]["p"].getValue(), "b" ) + + def testLoadThrowsOnMissingFile( self ) : + + s = Gaffer.ScriptNode() + s["r"] = Gaffer.Box() + + with self.assertRaisesRegex( Exception, "Could not find file 'thisFileDoesntExist.grf'" ) : + s["r"].loadReference( "thisFileDoesntExist.grf" ) + + def testHasMetadataEdit( self ) : + + s = Gaffer.ScriptNode() + s["b"] = Gaffer.Box() + s["b"]["p1"] = Gaffer.IntPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + s["b"]["p2"] = Gaffer.IntPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + + Gaffer.Metadata.registerValue( s["b"]["p1"], "referenced", "original" ) + Gaffer.Metadata.registerValue( s["b"]["p2"], "referenced", "original" ) + + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + s["r"] = Gaffer.Box() + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertFalse( s["r"].hasMetadataEdit( s["r"]["p1"], "referenced" ) ) + self.assertFalse( s["r"].hasMetadataEdit( s["r"]["p2"], "referenced" ) ) + + self.assertEqual( Gaffer.Metadata.value( s["r"]["p1"], "referenced" ), "original" ) + self.assertEqual( Gaffer.Metadata.value( s["r"]["p2"], "referenced" ), "original" ) + + with Gaffer.UndoScope( s ) : + Gaffer.Metadata.registerValue( s["r"]["p1"], "referenced", "override" ) + + self.assertTrue( s["r"].hasMetadataEdit( s["r"]["p1"], "referenced" ) ) + self.assertFalse( s["r"].hasMetadataEdit( s["r"]["p2"], "referenced" ) ) + + self.assertEqual( Gaffer.Metadata.value( s["r"]["p1"], "referenced" ), "override" ) + self.assertEqual( Gaffer.Metadata.value( s["r"]["p2"], "referenced" ), "original" ) + + with Gaffer.UndoScope( s ) : + Gaffer.Metadata.registerValue( s["r"]["p1"], "referenced", "foo" ) + Gaffer.Metadata.registerValue( s["r"]["p2"], "referenced", "bar" ) + + self.assertTrue( s["r"].hasMetadataEdit( s["r"]["p1"], "referenced" ) ) + self.assertTrue( s["r"].hasMetadataEdit( s["r"]["p2"], "referenced" ) ) + + self.assertEqual( Gaffer.Metadata.value( s["r"]["p1"], "referenced" ), "foo" ) + self.assertEqual( Gaffer.Metadata.value( s["r"]["p2"], "referenced" ), "bar" ) + + s.undo() + + self.assertTrue( s["r"].hasMetadataEdit( s["r"]["p1"], "referenced" ) ) + self.assertFalse( s["r"].hasMetadataEdit( s["r"]["p2"], "referenced" ) ) + + self.assertEqual( Gaffer.Metadata.value( s["r"]["p1"], "referenced" ), "override" ) + self.assertEqual( Gaffer.Metadata.value( s["r"]["p2"], "referenced" ), "original" ) + + s.undo() + + self.assertFalse( s["r"].hasMetadataEdit( s["r"]["p1"], "referenced" ) ) + self.assertFalse( s["r"].hasMetadataEdit( s["r"]["p2"], "referenced" ) ) + + self.assertEqual( Gaffer.Metadata.value( s["r"]["p1"], "referenced"), "original" ) + self.assertEqual( Gaffer.Metadata.value( s["r"]["p2"], "referenced"), "original" ) + + s.redo() + + self.assertTrue( s["r"].hasMetadataEdit( s["r"]["p1"], "referenced" ) ) + self.assertFalse( s["r"].hasMetadataEdit( s["r"]["p2"], "referenced" ) ) + + self.assertEqual( Gaffer.Metadata.value( s["r"]["p1"], "referenced" ), "override" ) + self.assertEqual( Gaffer.Metadata.value( s["r"]["p2"], "referenced" ), "original" ) + + s.redo() + + self.assertTrue( s["r"].hasMetadataEdit( s["r"]["p1"], "referenced" ) ) + self.assertTrue( s["r"].hasMetadataEdit( s["r"]["p2"], "referenced" ) ) + + self.assertEqual( Gaffer.Metadata.value( s["r"]["p1"], "referenced" ), "foo" ) + self.assertEqual( Gaffer.Metadata.value( s["r"]["p2"], "referenced" ), "bar" ) + + def testHasMetadataEditAfterReload( self ) : + + s = Gaffer.ScriptNode() + s["b"] = Gaffer.Box() + s["b"]["p"] = Gaffer.IntPlug( defaultValue = 1, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + Gaffer.Metadata.registerValue( s["b"]["p"], "referenced", "original" ) + + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + s["r"] = Gaffer.Box() + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + self.assertFalse( s["r"].hasMetadataEdit( s["r"]["p"], "referenced" ) ) + self.assertEqual( Gaffer.Metadata.value( s["r"]["p"], "referenced" ), "original" ) + + Gaffer.Metadata.registerValue( s["r"]["p"], "referenced", "override" ) + p = s["r"]["p"] + self.assertTrue( s["r"].hasMetadataEdit( p, "referenced" ) ) + self.assertTrue( s["r"].hasMetadataEdit( s["r"]["p"], "referenced" ) ) + self.assertEqual( Gaffer.Metadata.value( s["r"]["p"], "referenced" ), "override" ) + + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + # The old plug doesn't have an edit any more, because + # it doesn't even belong to the reference any more. + self.assertFalse( s["r"].isAncestorOf( p ) ) + self.assertFalse( s["r"].hasMetadataEdit( p, "referenced" ) ) + + # But the new plug does have one. + self.assertTrue( s["r"].hasMetadataEdit( s["r"]["p"], "referenced" ) ) + self.assertEqual( Gaffer.Metadata.value( s["r"]["p"], "referenced"), "override" ) + + def testPlugMetadataOverrides( self ) : + + s = Gaffer.ScriptNode() + s["b"] = Gaffer.Box() + s["b"]["p"] = Gaffer.IntPlug( defaultValue = 1, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + Gaffer.Metadata.registerValue( s["b"]["p"], "preset:Red", 0 ) + Gaffer.Metadata.registerValue( s["b"]["p"], "preset:Green", 1 ) + Gaffer.Metadata.registerValue( s["b"]["p"], "preset:Blue", 2 ) + + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + s["r"] = Gaffer.Box() + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + # Make sure the metadata exported on the plug was loaded by the Reference + self.assertEqual( Gaffer.Metadata.value( s["r"]["p"], "preset:Red" ), 0 ) + self.assertEqual( Gaffer.Metadata.value( s["r"]["p"], "preset:Green" ), 1 ) + self.assertEqual( Gaffer.Metadata.value( s["r"]["p"], "preset:Blue" ), 2 ) + + # Overriding one of the existing entries + Gaffer.Metadata.registerValue( s["r"]["p"], "preset:Green", 100 ) + # Add an entirely new metadata entry + Gaffer.Metadata.registerValue( s["r"]["p"], "preset:Alpha", 3 ) + self.assertEqual( Gaffer.Metadata.value( s["r"]["p"], "preset:Alpha" ), 3 ) + + # Change exported metadata and reload. + # When reloading, the existing data on the Reference is preserved. + # When creating new Reference, the data on the new instance is as per the referenced box. + Gaffer.Metadata.registerValue( s["b"]["p"], "preset:Blue", 42 ) + + s["b"].exportReference( self.temporaryDirectory() / "test2.grf" ) + s["r"].loadReference( self.temporaryDirectory() / "test2.grf" ) + + self.assertEqual( Gaffer.Metadata.value( s["r"]["p"], "preset:Red" ), 0 ) + self.assertEqual( Gaffer.Metadata.value( s["r"]["p"], "preset:Green" ), 100 ) + self.assertEqual( Gaffer.Metadata.value( s["r"]["p"], "preset:Blue" ), 42 ) # Reverted to referenced value + self.assertEqual( Gaffer.Metadata.value( s["r"]["p"], "preset:Alpha" ), 3 ) + + def assertMetadata( script ) : + + # Red hasn't been touched at all, expect value from the referenced box. + self.assertEqual( Gaffer.Metadata.value( script["r"]["p"], "preset:Red" ), 0 ) + self.assertFalse( s["r"].hasMetadataEdit( s["r"]["p"], "preset:Red" ) ) + + # Green has been overridden on the Reference node which needs to be preserved. + self.assertEqual( Gaffer.Metadata.value( script["r"]["p"], "preset:Green" ), 100 ) + self.assertTrue( s["r"].hasMetadataEdit( s["r"]["p"], "preset:Green" ) ) + + # Blue has been changed in the referenced box and the new value needs + # to come through as the old one hasn't been overridden. + self.assertEqual( Gaffer.Metadata.value( script["r"]["p"], "preset:Blue" ), 42 ) + self.assertFalse( s["r"].hasMetadataEdit( s["r"]["p"], "preset:Blue" ) ) + + # Alpha has been added, which needs to be preserved. + self.assertEqual( Gaffer.Metadata.value( script["r"]["p"], "preset:Alpha" ), 3 ) + self.assertTrue( s["r"].hasMetadataEdit( s["r"]["p"], "preset:Alpha" ) ) + + assertMetadata( s ) + + s2 = Gaffer.ScriptNode() + s2.execute( s.serialise() ) + + assertMetadata( s2 ) + + # Make sure serialising several times doesn't change the serialised + # data. This could happen if serialised edits wouldn't lead to recorded + # edits on the new reference. + + s3 = Gaffer.ScriptNode() + s3.execute( s2.serialise() ) + + assertMetadata( s3 ) + + def testChildPlugMetadata( self ) : + + s = Gaffer.ScriptNode() + s["b"] = Gaffer.Box() + s["b"]["p"] = Gaffer.Color3fPlug( defaultValue = imath.Color3f( 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + s["r"] = Gaffer.Box() + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + Gaffer.Metadata.registerValue( s["r"]["p"]["r"], "isRed", True ) + + s2 = Gaffer.ScriptNode() + s2.execute( s.serialise() ) + + # The metadata needs to be serialised. + self.assertEqual( Gaffer.Metadata.value( s2["r"]["p"]["r"], "isRed" ), True ) + + # It also needs to be transferred on reload + s["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + self.assertEqual( Gaffer.Metadata.value( s["r"]["p"]["r"], "isRed" ), True ) + + def testUserPlugMetadataSerialisation( self ) : + + s = Gaffer.ScriptNode() + + s["r"] = Gaffer.Box() + Gaffer.Metadata.registerValue( s["r"]["user"], "hasChild", True ) + + s["r"]["user"]["child"] = Gaffer.IntPlug( defaultValue = 1, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + Gaffer.Metadata.registerValue( s["r"]["user"]["child"], "hasChild", False ) + + # We don't register edits on user plugs. + self.assertFalse( s["r"].hasMetadataEdit( s["r"]["user"], "hasChild" ) ) + self.assertFalse( s["r"].hasMetadataEdit( s["r"]["user"]["child"], "hasChild" ) ) + + s2 = Gaffer.ScriptNode() + s2.execute( s.serialise() ) + + # We do, however, expect changes to be serialised. + self.assertEqual( Gaffer.Metadata.value( s2["r"]["user"], "hasChild" ), True ) + self.assertEqual( Gaffer.Metadata.value( s2["r"]["user"]["child"], "hasChild" ), False ) + + def testPlugPromotionPreservesMetadata( self ) : + + s = Gaffer.ScriptNode() + s["b"] = Gaffer.Box() + + s["b"]["p"] = Gaffer.Color3fPlug( defaultValue = imath.Color3f( 0 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + Gaffer.Metadata.registerValue( s["b"]["p"], "isColor", True ) + + s["b"].exportReference( self.temporaryDirectory() / "test.grf" ) + + s["b2"] = Gaffer.Box() + + s["b2"]["r"] = Gaffer.Box() + s["b2"]["r"].loadReference( self.temporaryDirectory() / "test.grf" ) + + Gaffer.PlugAlgo.promote( s["b2"]["r"]["p"] ) + + self.assertEqual( Gaffer.Metadata.value( s["b2"]["p"], "isColor" ), True ) + + def testPromotedSpreadsheetDefaultValues( self ) : + + script = Gaffer.ScriptNode() + script["box"] = Gaffer.Box() + + script["box"]["spreadsheet"] = Gaffer.Spreadsheet() + script["box"]["spreadsheet"]["rows"].addColumn( Gaffer.StringPlug( "string" ) ) + script["box"]["spreadsheet"]["rows"].addColumn( Gaffer.IntPlug( "int" ) ) + script["box"]["spreadsheet"]["rows"].addRows( 2 ) + + script["box"]["spreadsheet"]["rows"][0]["cells"]["string"]["value"].setValue( "default" ) + script["box"]["spreadsheet"]["rows"][0]["cells"]["int"]["value"].setValue( -1 ) + script["box"]["spreadsheet"]["rows"][1]["cells"]["string"]["value"].setValue( "one" ) + script["box"]["spreadsheet"]["rows"][1]["cells"]["int"]["value"].setValue( 1 ) + script["box"]["spreadsheet"]["rows"][2]["cells"]["string"]["value"].setValue( "two" ) + script["box"]["spreadsheet"]["rows"][2]["cells"]["int"]["value"].setValue( 2 ) + + script["box"]["spreadsheet"]["rows"][1]["name"].setValue( "row1" ) + script["box"]["spreadsheet"]["rows"][1]["enabled"].setValue( False ) + + Gaffer.PlugAlgo.promote( script["box"]["spreadsheet"]["rows"] ) + script["box"]["rows"].resetDefault() + + script["box"].exportReference( self.temporaryDirectory() / "test.grf" ) + + script["reference"] = Gaffer.Box() + script["reference"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertEqual( len( script["reference"]["rows"] ), len( script["box"]["rows"] ) ) + self.assertEqual( script["reference"]["rows"][0].keys(), script["box"]["rows"][0].keys() ) + + for row in range( 0, len( script["box"]["rows"] ) ) : + + self.assertEqual( + script["reference"]["rows"][row]["name"].defaultValue(), + script["box"]["rows"][row]["name"].defaultValue() + ) + self.assertEqual( + script["reference"]["rows"][row]["enabled"].defaultValue(), + script["box"]["rows"][row]["enabled"].defaultValue() + ) + + for column in range( 0, len( script["box"]["rows"][0]["cells"].keys() ) ) : + + self.assertEqual( + script["reference"]["rows"][row]["cells"][column]["value"].defaultValue(), + script["box"]["rows"][row]["cells"][column]["value"].defaultValue(), + ) + self.assertEqual( + script["reference"]["rows"][row]["cells"][column]["enabled"].defaultValue(), + script["box"]["rows"][row]["cells"][column]["enabled"].defaultValue(), + ) + + self.assertTrue( script["reference"]["rows"].isSetToDefault() ) + + def testPromotedSpreadsheetCopyPaste( self ) : + + script = Gaffer.ScriptNode() + script["box"] = Gaffer.Box() + + script["box"]["spreadsheet"] = Gaffer.Spreadsheet() + script["box"]["spreadsheet"]["rows"].addColumn( Gaffer.StringPlug( "string" ) ) + script["box"]["spreadsheet"]["rows"].addRow() + Gaffer.PlugAlgo.promote( script["box"]["spreadsheet"]["rows"] ) + + script["box"].exportReference( self.temporaryDirectory() / "test.grf" ) + + script["reference"] = Gaffer.Box() + script["reference"].loadReference( self.temporaryDirectory() / "test.grf" ) + script["reference"]["rows"][1]["cells"]["string"]["value"].setValue( "test" ) + + script.execute( script.serialise( filter = Gaffer.StandardSet( [ script["reference"] ] ) ) ) + + self.assertEqual( script["reference1"]["rows"].keys(), script["box"]["rows"].keys() ) + self.assertEqual( script["reference1"]["rows"][1]["cells"].keys(), script["box"]["rows"][1]["cells"].keys() ) + self.assertEqual( script["reference1"]["rows"][1]["cells"]["string"]["value"].getValue(), "test" ) + + def testTransformPlugs( self ) : + + script = Gaffer.ScriptNode() + script["box"] = Gaffer.Box() + + script["box"]["t2"] = Gaffer.Transform2DPlug( defaultTranslate = imath.V2f( 10, 11 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + script["box"]["t2"]["rotate"].setValue( 10 ) + script["box"]["t3"] = Gaffer.TransformPlug( defaultTranslate = imath.V3f( 10, 11, 12 ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + script["box"]["t3"]["rotate"].setValue( imath.V3f( 1, 2, 3 ) ) + + script["box"].exportReference( self.temporaryDirectory() / "test.grf" ) + + script["reference"] = Gaffer.Box() + script["reference"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertTrue( script["reference"]["t2"].isSetToDefault() ) + for name in script["reference"]["t2"].keys() : + self.assertEqual( script["reference"]["t2"][name].defaultValue(), script["box"]["t2"][name].defaultValue() ) + + self.assertTrue( script["reference"]["t3"].isSetToDefault() ) + for name in script["reference"]["t3"].keys() : + self.assertEqual( script["reference"]["t3"][name].defaultValue(), script["box"]["t3"][name].defaultValue() ) + + def testCompoundDataPlugs( self ) : + + script = Gaffer.ScriptNode() + script["box"] = Gaffer.Box() + + script["box"]["p"] = Gaffer.CompoundDataPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + script["box"]["p"]["m"] = Gaffer.NameValuePlug( "a", 10, defaultEnabled = True, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + self.assertEqual( script["box"]["p"]["m"]["name"].defaultValue(), "a" ) + self.assertEqual( script["box"]["p"]["m"]["value"].defaultValue(), 10 ) + self.assertEqual( script["box"]["p"]["m"]["enabled"].defaultValue(), True ) + script["box"]["p"]["m"]["name"].setValue( "b" ) + script["box"]["p"]["m"]["value"].setValue( 11 ) + script["box"]["p"]["m"]["enabled"].setValue( False ) + + script["box"].exportReference( self.temporaryDirectory() / "test.grf" ) + + script["reference"] = Gaffer.Box() + script["reference"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertTrue( script["reference"]["p"].isSetToDefault() ) + self.assertEqual( script["reference"]["p"].defaultHash(), script["box"]["p"].defaultHash() ) + + def testPromotedSpreadsheetDuplicateAsBox( self ) : + + script = Gaffer.ScriptNode() + + # Promote Spreadsheet to Box and export for referencing. + + script["box"] = Gaffer.Box() + + script["box"]["spreadsheet"] = Gaffer.Spreadsheet() + script["box"]["spreadsheet"]["rows"].addRow() + script["box"]["spreadsheet"]["rows"][1]["name"].setValue( "test" ) + Gaffer.PlugAlgo.promote( script["box"]["spreadsheet"]["rows"] ) + script["box"]["rows"].resetDefault() + + script["box"].exportReference( self.temporaryDirectory() / "test.grf" ) + + # Reference and then duplicate as box. This is using the same + # method as used by the "Duplicate as Box" menu in the UI. + + script["reference"] = Gaffer.Box() + script["reference"].loadReference( self.temporaryDirectory() / "test.grf" ) + + script["duplicate"] = Gaffer.Box() + script.executeFile( script["reference"].referenceFileName(), parent = script["duplicate"] ) + self.assertEqual( script["duplicate"]["rows"][1]["name"].defaultValue(), "test" ) + self.assertEqual( script["duplicate"]["rows"][1]["name"].getValue(), "test" ) + + # Now copy/paste the duplicated box. The row should have retained its name. + + script.execute( script.serialise( filter = Gaffer.StandardSet( [ script["duplicate"] ] ) ) ) + self.assertEqual( script["duplicate1"]["rows"][1]["name"].defaultValue(), "test" ) + self.assertEqual( script["duplicate1"]["rows"][1]["name"].getValue(), "test" ) + + def testSpreadsheetWithMixedDefaultAndValueEdits( self ) : + + script = Gaffer.ScriptNode() + + # Make box and promoted spreadsheet + + script["box"] = Gaffer.Box() + + script["box"]["spreadsheet"] = Gaffer.Spreadsheet() + script["box"]["spreadsheet"]["rows"].addColumn( Gaffer.V3iPlug( "c1", defaultValue = imath.V3i( 1, 2, 3 ) ) ) + script["box"]["spreadsheet"]["rows"].addRow() + promoted = Gaffer.PlugAlgo.promote( script["box"]["spreadsheet"]["rows"] ) + + # Mess with cell values and defaults + + promoted[1]["cells"]["c1"]["value"]["x"].setValue( 2 ) # Non-default value. Should be ignored on export. + promoted[1]["cells"]["c1"]["value"]["y"].setValue( 3 ) + promoted[1]["cells"]["c1"]["value"]["y"].resetDefault() # Modified default. Should be preserved on export. + promoted[1]["cells"]["c1"]["value"]["z"].setValue( 4 ) + promoted[1]["cells"]["c1"]["value"]["z"].resetDefault() # Modified default. Should be preserved on export. + + script["box"].exportReference( self.temporaryDirectory() / "test.grf" ) + + script["reference"] = Gaffer.Box() + script["reference"].loadReference( self.temporaryDirectory() / "test.grf" ) + + self.assertTrue( script["reference"]["rows"].isSetToDefault() ) + self.assertEqual( script["reference"]["rows"][1]["cells"]["c1"]["value"]["x"].getValue(), 1 ) + self.assertEqual( script["reference"]["rows"][1]["cells"]["c1"]["value"]["y"].getValue(), 3 ) + self.assertEqual( script["reference"]["rows"][1]["cells"]["c1"]["value"]["z"].getValue(), 4 ) + + def testRampPlug( self ) : + + ramps = [ + IECore.Rampff( + ( + ( 0, 0 ), + ( 0.2, 0.3 ), + ( 0.4, 0.9 ), + ( 1, 1 ), + ), + IECore.RampInterpolation.CatmullRom + ), + IECore.Rampff( + ( + ( 1, 1 ), + ( 1, 1 ), + ( 0.2, 0.3 ), + ( 0.4, 0.9 ), + ( 0, 0 ), + ( 0, 0 ), + ), + IECore.RampInterpolation.Linear + ) + ] + + fileName = self.temporaryDirectory() / "test.grf" + + for nonDefaultAtExport in ( False, True ) : + + for i in range( 0, 2 ) : + + # On one iteration `defaultValue` has more points, + # and on the other iteration `otherValue` has more + # points. This is useful for catching bugs because + # RampPlugs must add plugs to represent points. + defaultValue = ramps[i] + otherValue = ramps[(i+1)%2] + + script = Gaffer.ScriptNode() + + # Create Box with RampPlug + + script["box"] = Gaffer.Box() + script["box"]["ramp"] = Gaffer.RampffPlug( defaultValue = defaultValue, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + if nonDefaultAtExport : + script["box"]["ramp"].setValue( otherValue ) + script["box"].exportReference( fileName ) + + # Reference it and check we get what we want + + script["reference"] = Gaffer.Box() + script["reference"].loadReference( fileName ) + + self.assertEqual( script["reference"]["ramp"].getValue(), defaultValue ) + self.assertEqual( script["reference"]["ramp"].defaultValue(), defaultValue ) + self.assertTrue( script["reference"]["ramp"].isSetToDefault() ) + + # Set value on reference and save and reload the script, checking that + # the newly opened script also has the edited value. + + script["reference"]["ramp"].setValue( otherValue ) + self.assertEqual( script["reference"]["ramp"].getValue(), otherValue ) + + script2 = Gaffer.ScriptNode() + script2.execute( script.serialise() ) + + self.assertEqual( script2["reference"]["ramp"].getValue(), otherValue ) + self.assertEqual( script2["reference"]["ramp"].defaultValue(), defaultValue ) + self.assertFalse( script2["reference"]["ramp"].isSetToDefault() ) + + # Reload the reference, and check we kept the edited value. + + script["reference"].loadReference( fileName ) + self.assertEqual( script["reference"]["ramp"].getValue(), otherValue ) + + # Change default value on box and re-export. + + script["box"]["ramp"].setValue( otherValue ) + script["box"]["ramp"].resetDefault() + script["box"].exportReference( fileName ) + + # If the reference doesn't have an edit to the value, + # then it should pick up the new value on a reload. + + script["reference"]["ramp"].setToDefault() + self.assertEqual( script["reference"]["ramp"].getValue(), defaultValue ) + + script["reference"].loadReference( fileName ) + self.assertEqual( script["reference"]["ramp"].getValue(), otherValue ) + self.assertEqual( script["reference"]["ramp"].defaultValue(), otherValue ) + self.assertTrue( script["reference"]["ramp"].isSetToDefault() ) + + def testRampPlugUpgradeDefault( self ) : + + script = Gaffer.ScriptNode() + + defaultOne = IECore.Rampff( + ( + ( 0, 0 ), + ( 0.2, 0.3 ), + ( 0.4, 0.9 ), + ( 1, 1 ), + ), + IECore.RampInterpolation.CatmullRom + ) + + defaultTwo = IECore.Rampff( + ( + ( 1, 1 ), + ( 1, 1 ), + ( 0.2, 0.3 ), + ( 0.4, 0.9 ), + ( 0, 0 ), + ( 0, 0 ), + ), + IECore.RampInterpolation.Linear + ) + + fileName = self.temporaryDirectory() / "test.grf" + script = Gaffer.ScriptNode() + + # Export a box with a ramp on it + + script["box"] = Gaffer.Box() + script["box"]["ramp"] = Gaffer.RampffPlug( defaultValue = defaultOne, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + script["box"].exportReference( fileName ) + + # Make reference1 at default ramp value, reference2 with modified ramp value + + script["reference1"] = Gaffer.Box() + script["reference1"].loadReference( fileName ) + self.assertEqual( script["reference1"]["ramp"].defaultValue(), script["box"]["ramp"].defaultValue() ) + self.assertTrue( script["reference1"]["ramp"].isSetToDefault() ) + + script["reference2"] = Gaffer.Box() + script["reference2"].loadReference( fileName ) + self.assertEqual( script["reference2"]["ramp"].defaultValue(), script["box"]["ramp"].defaultValue() ) + self.assertTrue( script["reference2"]["ramp"].isSetToDefault() ) + script["reference2"]["ramp"].pointPlug( 0 )["y"].setValue( 100 ) + self.assertFalse( script["reference2"]["ramp"].isSetToDefault() ) + reference2Value = script["reference2"]["ramp"].getValue() + + # Export a new version with a different default. This should + # be inherited by reference1 and overridden by reference2. + + script["box"]["ramp"].setValue( defaultTwo ) + script["box"]["ramp"].resetDefault() + script["box"].exportReference( fileName ) + + script["reference1"].loadReference( fileName ) + self.assertEqual( script["reference1"]["ramp"].defaultValue(), script["box"]["ramp"].defaultValue() ) + self.assertTrue( script["reference1"]["ramp"].isSetToDefault() ) + + script["reference2"].loadReference( fileName ) + self.assertEqual( script["reference2"]["ramp"].defaultValue(), script["box"]["ramp"].defaultValue() ) + self.assertFalse( script["reference2"]["ramp"].isSetToDefault() ) + self.assertEqual( script["reference2"]["ramp"].getValue(), reference2Value ) + + def testAddingChildPlugs( self ) : + + # Export a box with a CompoundDataPlug and RowsPlug, + # both without additional members/rows. + + script = Gaffer.ScriptNode() + + script["box"] = Gaffer.Box() + + script["box"]["spreadsheet"] = Gaffer.Spreadsheet() + script["box"]["spreadsheet"]["rows"].addColumn( Gaffer.IntPlug( "c1" ) ) + Gaffer.PlugAlgo.promote( script["box"]["spreadsheet"]["rows"] ) + + script["box"]["node"] = Gaffer.Node() + script["box"]["node"]["user"]["compoundData"] = Gaffer.CompoundDataPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + Gaffer.PlugAlgo.promoteWithName( script["box"]["node"]["user"]["compoundData"], "compoundData" ) + + # also test promotion not directly to the box, but to a plug inside the box + + script["box"]["container"] = Gaffer.Plug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + script["box"]["spreadsheetInPlug"] = Gaffer.Spreadsheet() + script["box"]["spreadsheetInPlug"]["rows"].addColumn( Gaffer.IntPlug( "c1" ) ) + Gaffer.PlugAlgo.promote( script["box"]["spreadsheetInPlug"]["rows"], parent = script["box"]["container"] ) + + fileName = self.temporaryDirectory() / "test.grf" + script["box"].exportReference( fileName ) + + # Load it onto a Reference, and add some members/rows. + + script["reference"] = Gaffer.Box() + script["reference"].loadReference( fileName ) + + script["reference"]["rows"].addRows( 2 ) + for i, row in enumerate( script["reference"]["rows"] ) : + row["cells"]["c1"]["value"].setValue( i ) + + script["reference"]["compoundData"]["m1"] = Gaffer.NameValuePlug( "test1", 10, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + script["reference"]["compoundData"]["m2"] = Gaffer.NameValuePlug( "test2", 20, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + + script["reference"]["container"]["rows"].addRows( 2 ) + for i, row in enumerate( script["reference"]["container"]["rows"] ) : + row["cells"]["c1"]["value"].setValue( i ) + + def assertExpectedChildren( reference ) : + + self.assertEqual( len( reference["rows"] ), 3 ) + for i, row in enumerate( reference["rows"] ) : + self.assertEqual( row["cells"]["c1"]["value"].getValue(), i ) + self.assertEqual( reference.isChildEdit( row ), i > 0 ) + + self.assertEqual( len( reference["spreadsheet"]["rows"] ), 3 ) + self.assertEqual( reference["spreadsheet"]["rows"].getInput(), reference["rows"] ) + + self.assertEqual( len( reference["compoundData"] ), 2 ) + self.assertEqual( reference["compoundData"]["m1"]["name"].getValue(), "test1" ) + self.assertEqual( reference["compoundData"]["m1"]["value"].getValue(), 10 ) + self.assertEqual( reference["compoundData"]["m2"]["name"].getValue(), "test2" ) + self.assertEqual( reference["compoundData"]["m2"]["value"].getValue(), 20 ) + self.assertTrue( reference.isChildEdit( reference["compoundData"]["m1"] ) ) + self.assertTrue( reference.isChildEdit( reference["compoundData"]["m2"] ) ) + + self.assertEqual( len( reference["node"]["user"]["compoundData"] ), 2 ) + self.assertEqual( reference["node"]["user"]["compoundData"].getInput(), reference["compoundData"] ) + + self.assertFalse( reference.isChildEdit( reference["rows"] ) ) + self.assertFalse( reference.isChildEdit( reference["compoundData"] ) ) + self.assertFalse( reference.isChildEdit( reference["compoundData"]["m1"]["value"] ) ) + + self.assertEqual( len( reference["container"]["rows"] ), 3 ) + for i, row in enumerate( reference["container"]["rows"] ) : + self.assertEqual( row["cells"]["c1"]["value"].getValue(), i ) + self.assertEqual( reference.isChildEdit( row ), i > 0 ) + + self.assertEqual( len( reference["spreadsheetInPlug"]["rows"] ), 3 ) + self.assertEqual( reference["spreadsheetInPlug"]["rows"].getInput(), reference["container"]["rows"] ) + + assertExpectedChildren( script["reference"] ) + + # Reload the reference, and check that our edits have been kept. + + script["reference"].loadReference( fileName ) + assertExpectedChildren( script["reference"] ) + + # Do it again, to be sure edit tracking has been maintained + # across reload. + + script["reference"].loadReference( fileName ) + assertExpectedChildren( script["reference"] ) + + # Save and reload the script, and check our edits have been kept. + + script2 = Gaffer.ScriptNode() + script2.execute( script.serialise() ) + assertExpectedChildren( script2["reference"] ) + + def testAddAndRemoveSpreadsheetColumns( self ) : + + script = Gaffer.ScriptNode() + + script["box"] = Gaffer.Box() + + script["box"]["spreadsheet"] = Gaffer.Spreadsheet() + script["box"]["spreadsheet"]["rows"].addColumn( Gaffer.IntPlug( "c1" ) ) + script["box"]["spreadsheet"]["rows"].addColumn( Gaffer.FloatPlug( "c2" ) ) + Gaffer.PlugAlgo.promote( script["box"]["spreadsheet"]["rows"] ) + + fileName = self.temporaryDirectory() / "test.grf" + script["box"].exportReference( fileName ) + + script["reference"] = Gaffer.Box() + script["reference"].loadReference( fileName ) + + script["reference"]["rows"].addRows( 3 ) + + for i, row in enumerate( script["reference"]["rows"] ) : + row["cells"]["c1"]["value"].setValue( i ) + row["cells"]["c2"]["value"].setValue( i + 1 ) + + def assertCellValues( referencedRows, removedColumns = {} ) : + + for i, row in enumerate( script["reference"]["rows"] ) : + if "c1" not in removedColumns : + self.assertEqual( row["cells"]["c1"]["value"].getValue(), i ) + if "c2" not in removedColumns : + self.assertEqual( row["cells"]["c2"]["value"].getValue(), i + 1 ) + + def assertColumnsMatch( referencedRows, expectedRow ) : + + for row in referencedRows : + self.assertEqual( len( row["cells"] ), len( expectedRow["cells"] ) ) + for i, cell in enumerate( row["cells"] ) : + self.assertEqual( cell.getName(), expectedRow["cells"][i].getName() ) + self.assertEqual( repr( cell["value"] ), repr( expectedRow["cells"][i]["value"] ) ) + + assertCellValues( script["reference"]["rows"] ) + assertColumnsMatch( script["reference"]["rows"], script["box"]["rows"].defaultRow() ) + + script["box"]["rows"].addColumn( Gaffer.StringPlug( "c3" ) ) + script["box"]["rows"].addColumn( Gaffer.BoolPlug( "c4" ) ) + script["box"]["rows"].removeColumn( 1 ) # Remove "c2" + script["box"].exportReference( fileName ) + + script["reference"].loadReference( fileName ) + self.assertTrue( script["reference"].isReference() ) + self.assertEqual( script["reference"].referenceFileName(), fileName ) + assertCellValues( script["reference"]["rows"], removedColumns = { "c2" } ) + assertColumnsMatch( script["reference"]["rows"], script["box"]["rows"].defaultRow() ) + + script2 = Gaffer.ScriptNode() + script2.execute( script.serialise() ) + assertColumnsMatch( script2["reference"]["rows"], script["box"]["rows"].defaultRow() ) + + def testChildNodesAreReadOnlyMetadata( self ) : + + s = Gaffer.ScriptNode() + + s["n1"] = GafferTest.AddNode() + + b = Gaffer.Box.create( s, Gaffer.StandardSet( [ s["n1"] ] ) ) + self.assertFalse( Gaffer.MetadataAlgo.getChildNodesAreReadOnly( b ) ) + + b.exportReference( self.temporaryDirectory() / "test.grf" ) + + s["r1"] = Gaffer.Box() + self.assertFalse( Gaffer.MetadataAlgo.getChildNodesAreReadOnly( s["r1"] ) ) + s["r1"].loadReference( self.temporaryDirectory() / "test.grf" ) + self.assertTrue( Gaffer.MetadataAlgo.getChildNodesAreReadOnly( s["r1"] ) ) + + # bake in the metadata into the Box to test if it will be handled by the Reference + Gaffer.MetadataAlgo.setChildNodesAreReadOnly( b, False ) + + b.exportReference( self.temporaryDirectory() / "testWithMetadata.grf" ) + + s["r2"] = Gaffer.Box() + s["r2"].loadReference( self.temporaryDirectory() / "testWithMetadata.grf" ) + + self.assertTrue( Gaffer.MetadataAlgo.getChildNodesAreReadOnly( s["r2"] ) ) + + def testInternalConnectionsNotSerialised( self ) : + + script = Gaffer.ScriptNode() + + script["box"] = Gaffer.Box() + # Testing both `In` and `Out` plugs, because although logically + # outputs should have `direction == Out`, in practice the UI only + # lets folks make input plugs, so that's what they use. + script["box"]["p1"] = Gaffer.IntPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + script["box"]["p2"] = Gaffer.IntPlug( direction = Gaffer.Plug.Direction.Out, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + script["box"]["__add1"] = GafferTest.AddNode() + script["box"]["__add2"] = GafferTest.AddNode() + script["box"]["p1"].setInput( script["box"]["__add1"]["sum"] ) + script["box"]["p2"].setInput( script["box"]["__add2"]["sum"] ) + + fileName = self.temporaryDirectory() / "test.grf" + script["box"].exportReference( fileName ) + + script["reference"] = Gaffer.Box() + script["reference"].loadReference( fileName ) + + self.assertEqual( script["reference"]["p1"].getInput(), script["reference"]["__add1"]["sum"] ) + self.assertEqual( script["reference"]["p2"].getInput(), script["reference"]["__add2"]["sum"] ) + + serialisation = script.serialise( filter = Gaffer.StandardSet( [ script["reference" ] ] ) ) + self.assertNotIn( "setInput", serialisation ) + + script2 = Gaffer.ScriptNode() + script2.execute( serialisation ) + + self.assertEqual( script2["reference"]["p1"].getInput(), script2["reference"]["__add1"]["sum"] ) + self.assertEqual( script2["reference"]["p2"].getInput(), script2["reference"]["__add2"]["sum"] ) + + def testChangeInternalConnection( self ) : + + # Publish reference with internal connection to output plug, and check + # that it can be loaded by a Reference node. + + script = Gaffer.ScriptNode() + + script["box"] = Gaffer.Box() + script["box"]["p"] = Gaffer.IntPlug( direction = Gaffer.Plug.Direction.Out, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + script["box"]["__add1"] = GafferTest.AddNode() + script["box"]["__add2"] = GafferTest.AddNode() + script["box"]["p"].setInput( script["box"]["__add1"]["sum"] ) + + fileName = self.temporaryDirectory() / "test.grf" + script["box"].exportReference( fileName ) + + script["reference"] = Gaffer.Box() + script["reference"].loadReference( fileName ) + self.assertEqual( script["reference"]["p"].getInput(), script["reference"]["__add1"]["sum"] ) + + # Republish the reference with a different internal connection. + + script["box"]["p"].setInput( script["box"]["__add2"]["sum"] ) + script["box"].exportReference( fileName ) + + import shutil + shutil.copy( fileName, "/tmp/test.gfr" ) + + # Check that if we serialise and reload the Reference node, we + # pick up the new internal connection. + + script2 = Gaffer.ScriptNode() + script2.execute( script.serialise() ) + self.assertEqual( script2["reference"]["p"].getInput(), script2["reference"]["__add2"]["sum"] ) + + def testExternalSiblingConnectionsPreserved( self ) : + + script = Gaffer.ScriptNode() + script["box"] = Gaffer.Box() + script["box"]["p1"] = Gaffer.IntPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + script["box"]["p2"] = Gaffer.IntPlug( flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) + + fileName = self.temporaryDirectory() / "test.grf" + script["box"].exportReference( fileName ) + + script["reference"] = Gaffer.Box() + script["reference"].loadReference( fileName ) + script["reference"]["p2"].setInput( script["reference"]["p1"] ) + + script2 = Gaffer.ScriptNode() + script2.execute( script.serialise() ) + self.assertEqual( script2["reference"]["p2"].getInput(), script2["reference"]["p1"] ) + if __name__ == "__main__": unittest.main() diff --git a/src/Gaffer/SubGraph.cpp b/src/Gaffer/SubGraph.cpp index c9024280e7a..88a9ac57355 100644 --- a/src/Gaffer/SubGraph.cpp +++ b/src/Gaffer/SubGraph.cpp @@ -582,6 +582,11 @@ void SubGraph::loadReference( const std::filesystem::path &fileName ) ); } +bool SubGraph::isReference() const +{ + return m_referenceState && !m_referenceState->fileName.empty(); +} + const std::filesystem::path &SubGraph::referenceFileName() const { return m_referenceState ? m_referenceState->fileName : g_emptyPath; @@ -763,8 +768,18 @@ void SubGraph::loadReferenceInternal( const std::filesystem::path &fileName ) if( !path.empty() ) { PlugEdits::LoadingScope loadingScope( m_referenceState->plugEdits ); + // We register our child nodes as read-only _before_ loading, to facilitate + // a special case in `MetadataAlgo::setNumericBookmark()`. Coverage for this + // is in `MetadataAlgoTest.testNumericBookmarksInReferences`. + Metadata::registerValue( this, g_childNodesAreReadOnlyName, new BoolData( true ), /* persistent = */ false ); errors = script->executeFile( path.string(), this, /* continueOnError = */ true ); - // deregister "childNodesAreReadOnly" metadata, in case it was baked in the exported file + // Alas we have to register again _after_ loading for `SubGraphTest.testChildNodesAreReadOnlyMetadata` + // to pass. That test appears to model a problem with an internal Image Engine node - ideally the issue + // would be fixed there and we'd remove this. See #4320. + Metadata::registerValue( this, g_childNodesAreReadOnlyName, new BoolData( true ), /* persistent = */ false ); + } + else + { Metadata::deregisterValue( this, g_childNodesAreReadOnlyName ); } diff --git a/src/GafferBindings/MetadataBinding.cpp b/src/GafferBindings/MetadataBinding.cpp index e4e8968a0e4..654cf71e3c9 100644 --- a/src/GafferBindings/MetadataBinding.cpp +++ b/src/GafferBindings/MetadataBinding.cpp @@ -46,7 +46,7 @@ #include "Gaffer/MetadataAlgo.h" #include "Gaffer/Node.h" #include "Gaffer/Plug.h" -#include "Gaffer/Reference.h" +#include "Gaffer/SubGraph.h" #include "IECorePython/ScopedGILLock.h" @@ -69,8 +69,8 @@ std::string metadataSerialisation( const Gaffer::GraphComponent *graphComponent, const std::vector keys = Metadata::registeredValues( graphComponent, Metadata::RegistrationTypes::InstancePersistent ); const Plug *plug = runTimeCast( graphComponent ); - const Reference *reference = plug ? runTimeCast( plug->node() ) : nullptr; - bool requireEdits = reference && plug && plug != reference->userPlug() && !reference->userPlug()->isAncestorOf( plug ); + const SubGraph *subGraph = plug ? runTimeCast( plug->node() ) : nullptr; + bool requireEdits = subGraph && subGraph->isReference() && plug && plug != subGraph->userPlug() && !subGraph->userPlug()->isAncestorOf( plug ); std::string result; for( std::vector::const_iterator it = keys.begin(), eIt = keys.end(); it != eIt; ++it ) @@ -78,7 +78,7 @@ std::string metadataSerialisation( const Gaffer::GraphComponent *graphComponent, // Metadata on Plugs that live on References only need to be // serialised if they have been edited after loading the reference. // Metadata on user plugs will always be serialised. - if( requireEdits && !reference->hasMetadataEdit( plug, *it ) ) + if( requireEdits && !subGraph->hasMetadataEdit( plug, *it ) ) { continue; } diff --git a/src/GafferBindings/PlugBinding.cpp b/src/GafferBindings/PlugBinding.cpp index d4b653ead65..6884874f32e 100644 --- a/src/GafferBindings/PlugBinding.cpp +++ b/src/GafferBindings/PlugBinding.cpp @@ -43,7 +43,7 @@ #include "Gaffer/Dot.h" #include "Gaffer/Node.h" #include "Gaffer/Plug.h" -#include "Gaffer/Reference.h" +#include "Gaffer/SubGraph.h" #include "Gaffer/Switch.h" #include "GafferBindings/PlugBinding.h" @@ -147,12 +147,12 @@ bool shouldSerialiseInput( const Plug *plug, const Serialisation &serialisation return false; } } - else if( auto reference = runTimeCast( plug->node() ) ) + else if( auto subGraph = runTimeCast( plug->node() ) ) { - if( reference->isAncestorOf( plug->getInput()->node() ) ) + if( subGraph->isReference() && subGraph->isAncestorOf( plug->getInput()->node() ) ) { // Don't serialise connection from a plug on an internal node - // onto an external plug of the Reference. These will have been + // onto an external plug of the reference. These will have been // serialised in the `.grf` file itself. return false; } diff --git a/src/GafferModule/SpreadsheetBinding.cpp b/src/GafferModule/SpreadsheetBinding.cpp index e3409bc42a4..03d12ad4475 100644 --- a/src/GafferModule/SpreadsheetBinding.cpp +++ b/src/GafferModule/SpreadsheetBinding.cpp @@ -39,7 +39,7 @@ #include "SpreadsheetBinding.h" #include "Gaffer/Metadata.h" -#include "Gaffer/Reference.h" +#include "Gaffer/SubGraph.h" #include "GafferBindings/DependencyNodeBinding.h" #include "GafferBindings/ValuePlugBinding.h" @@ -114,17 +114,17 @@ class RowsPlugSerialiser : public ValuePlugSerialiser { std::string result = ValuePlugSerialiser::postConstructor( graphComponent, identifier, serialisation ); const auto *plug = static_cast( graphComponent ); - const auto *reference = IECore::runTimeCast( plug->node() ); + const auto *subGraph = IECore::runTimeCast( plug->node() ); // Serialise columns IECore::ConstBoolDataPtr columnsNeedSerialisation = Metadata::value( plug, "spreadsheet:columnsNeedSerialisation" ); - if( reference ) + if( subGraph && subGraph->isReference() ) { // We don't currently allow users to add new columns to // referenced spreadsheets - the referenced columns will be - // created in `Reference::loadReference()`, so don't need to + // created in `SubGraph::loadReference()`, so don't need to // be serialised here. } else if( columnsNeedSerialisation && !columnsNeedSerialisation->readable() ) @@ -162,7 +162,7 @@ class RowsPlugSerialiser : public ValuePlugSerialiser for( size_t rowIndex = 1; rowIndex < plug->children().size(); ++rowIndex ) { const auto *row = plug->getChild( rowIndex ); - if( reference ) + if( subGraph && subGraph->isReference() ) { // References typically add rows in `loadReference()`, and we don't need to serialise // those. But we also allow users to add rows as edits on top of the reference, and @@ -171,7 +171,7 @@ class RowsPlugSerialiser : public ValuePlugSerialiser /// from nodes exported by ExtensionAlgo and any other nodes that might want to add /// a pre-populated RowsPlug in a constructor. We are deliberately not using the Dynamic /// flag for this as we are trying to phase it out. - numRowsToAdd += reference->isChildEdit( row ); + numRowsToAdd += subGraph->isChildEdit( row ); } else { diff --git a/src/GafferModule/SubGraphBinding.cpp b/src/GafferModule/SubGraphBinding.cpp index 7919271d18f..f59f002279f 100644 --- a/src/GafferModule/SubGraphBinding.cpp +++ b/src/GafferModule/SubGraphBinding.cpp @@ -53,20 +53,24 @@ using namespace IECorePython; using namespace Gaffer; using namespace GafferBindings; +// SubGraph +// ======== + namespace { -// Box -// === - -class BoxSerialiser : public NodeSerialiser +class SubGraphSerialiser : public NodeSerialiser { bool childNeedsSerialisation( const Gaffer::GraphComponent *child, const Serialisation &serialisation ) const override { if( child->isInstanceOf( Node::staticTypeId() ) ) { - return true; + const SubGraph *subGraph = static_cast( child->parent() ); + if( !subGraph->isReference() ) + { + return true; + } } return NodeSerialiser::childNeedsSerialisation( child, serialisation ); } @@ -75,11 +79,59 @@ class BoxSerialiser : public NodeSerialiser { if( child->isInstanceOf( Node::staticTypeId() ) ) { - return true; + const SubGraph *subGraph = static_cast( child->parent() ); + if( !subGraph->isReference() ) + { + return true; + } } return NodeSerialiser::childNeedsConstruction( child, serialisation ); } + std::string postConstructor( const Gaffer::GraphComponent *graphComponent, const std::string &identifier, Serialisation &serialisation ) const override + { + const SubGraph *subGraph = static_cast( graphComponent ); + + const std::filesystem::path &fileName = subGraph->referenceFileName(); + if( fileName.empty() ) + { + return ""; + }; + + if( IECore::runTimeCast( subGraph ) ) + { + // Serialise using deprecated method so we can load again in Gaffer 1.6 + // if needed. + /// \todo Remove + return identifier + ".load( \"" + fileName.generic_string() + "\" )\n"; + } + else + { + return identifier + ".loadReference( \"" + fileName.generic_string() + "\" )\n"; + } + } + +}; + +void loadReferenceWrapper( SubGraph &subGraph, const std::filesystem::path &fileName ) +{ + IECorePython::ScopedGILRelease gilRelease; + subGraph.loadReference( fileName ); +} + +struct ReferenceChangedSlotCaller +{ + void operator()( boost::python::object slot, SubGraphPtr subGraph ) + { + try + { + slot( subGraph ); + } + catch( const error_already_set & ) + { + IECorePython::ExceptionAlgo::translatePythonException(); + } + } }; } // namespace @@ -259,21 +311,6 @@ void registerProcessor( const std::string &name, object creator ) namespace { -struct ReferenceChangedSlotCaller -{ - void operator()( boost::python::object slot, SubGraphPtr subGraph ) - { - try - { - slot( subGraph ); - } - catch( const error_already_set & ) - { - IECorePython::ExceptionAlgo::translatePythonException(); - } - } -}; - struct ReferenceLoadedSlotCaller { void operator()( boost::python::object slot, ReferencePtr r ) @@ -289,30 +326,6 @@ struct ReferenceLoadedSlotCaller } }; -class ReferenceSerialiser : public NodeSerialiser -{ - - std::string postConstructor( const Gaffer::GraphComponent *graphComponent, const std::string &identifier, Serialisation &serialisation ) const override - { - const Reference *r = static_cast( graphComponent ); - - const std::filesystem::path &fileName = r->fileName(); - if( fileName.empty() ) - { - return ""; - }; - - return identifier + ".load( \"" + fileName.generic_string() + "\" )\n"; - } - -}; - -void loadReferenceWrapper( SubGraph &subGraph, const std::filesystem::path &fileName ) -{ - IECorePython::ScopedGILRelease gilRelease; - subGraph.loadReference( fileName ); -} - void load( Reference &r, const std::filesystem::path &f ) { IECorePython::ScopedGILRelease gilRelease; @@ -325,8 +338,9 @@ void GafferModule::bindSubGraph() { using SubGraphWrapper = DependencyNodeWrapper; DependencyNodeClass() - .def( "exportReference", &Box::exportReference ) + .def( "exportReference", &SubGraph::exportReference ) .def( "loadReference", &loadReferenceWrapper ) + .def( "isReference", &SubGraph::isReference ) .def( "referenceFileName", &SubGraph::referenceFileName, return_value_policy() ) .def( "referenceChangedSignal", &SubGraph::referenceChangedSignal, return_internal_reference<1>() ) .def( "hasMetadataEdit", &SubGraph::hasMetadataEdit ) @@ -335,6 +349,8 @@ void GafferModule::bindSubGraph() SignalClass, ReferenceChangedSlotCaller >( "ReferenceChangedSignal" ); + Serialisation::registerSerialiser( SubGraph::staticTypeId(), new SubGraphSerialiser ); + using BoxWrapper = DependencyNodeWrapper; DependencyNodeClass() @@ -343,8 +359,6 @@ void GafferModule::bindSubGraph() .staticmethod( "create" ) ; - Serialisation::registerSerialiser( Box::staticTypeId(), new BoxSerialiser ); - NodeClass( nullptr, no_init ) .def( "setup", &setup, ( arg( "plug" ) = object() ) ) .def( "setupPromotedPlug", &setupPromotedPlug ) @@ -371,8 +385,6 @@ void GafferModule::bindSubGraph() SignalClass, ReferenceLoadedSlotCaller >( "ReferenceLoadedSignal" ); - Serialisation::registerSerialiser( Reference::staticTypeId(), new ReferenceSerialiser ); - NodeClass() .def( "setup", &setup, ( arg( "plug" ) ) ) .def( "acquireProcessor", &acquireProcessor, ( arg( "type" ), arg( "createIfNecessary" ) = true ) ) From 5304ab48e1cff1fae507951d1dff3dae03610f4c Mon Sep 17 00:00:00 2001 From: John Haddon Date: Wed, 22 Apr 2026 12:54:58 +0100 Subject: [PATCH 6/7] SubGraph : Consolidate logic around what is referenceable - Replace `isReferencePlug()` with `isReferenceable()`, supporting nodes too. - Use `isReferenceable()` in `exportReference()` instead of duplicating logic. - Remove code for dealing with archaic `.grf` files with plugs parented under `user`. These date from prior to version 0.18.0.0. --- Changes.md | 1 + include/Gaffer/SubGraph.h | 2 +- src/Gaffer/SubGraph.cpp | 99 ++++++++++++--------------------------- 3 files changed, 32 insertions(+), 70 deletions(-) diff --git a/Changes.md b/Changes.md index 24d7d727d44..59007b26772 100644 --- a/Changes.md +++ b/Changes.md @@ -96,6 +96,7 @@ Breaking Changes - Light : Removed public constructor. Lights may now only be constructed via derived classes, which are now responsible for providing a Shader node to the base class. - OSLCode : Removed `shaderCompiledSignal()`. - PathColumn : Changed `headerData()` signature. +- Reference : Removed support for loading `.grf` files from versions prior to 0.18.0.0. Build ----- diff --git a/include/Gaffer/SubGraph.h b/include/Gaffer/SubGraph.h index 2f8c0ad9ad4..fd6e0df0a69 100644 --- a/include/Gaffer/SubGraph.h +++ b/include/Gaffer/SubGraph.h @@ -117,7 +117,7 @@ class GAFFER_API SubGraph : public DependencyNode private : void loadReferenceInternal( const std::filesystem::path &fileName ); - bool isReferencePlug( const Plug *plug ) const; + bool isReferenceable( const GraphComponent *descendant ) const; ReferenceChangedSignal m_referenceChangedSignal; diff --git a/src/Gaffer/SubGraph.cpp b/src/Gaffer/SubGraph.cpp index 88a9ac57355..40bd641f8a1 100644 --- a/src/Gaffer/SubGraph.cpp +++ b/src/Gaffer/SubGraph.cpp @@ -325,7 +325,7 @@ class SubGraph::PlugEdits : public Signals::Trackable return &(it->second); } - if( !m_subGraph->isReferencePlug( plug ) ) + if( !m_subGraph->isReferenceable( plug ) ) { // We'll allow retrieval of existing edits on this plug, but we // won't create new ones. @@ -399,7 +399,7 @@ class SubGraph::PlugEdits : public Signals::Trackable { for( auto &plug : Plug::RecursiveRange( *m_subGraph ) ) { - if( !m_subGraph->isReferencePlug( plug.get() ) ) + if( !m_subGraph->isReferenceable( plug.get() ) ) { continue; } @@ -527,26 +527,12 @@ void SubGraph::exportReference( const std::filesystem::path &fileName ) const throw IECore::Exception( "SubGraph::exportForReference called without ScriptNode" ); } - // we only want to save out our child nodes and plugs that are visible in the UI, so we build a filter - // to specify just the things to export. - - boost::regex invisiblePlug( "^__.*$" ); StandardSetPtr toExport = new StandardSet; - for( ChildIterator it = children().begin(), eIt = children().end(); it != eIt; ++it ) + for( const auto &child : GraphComponent::Range( *this ) ) { - if( (*it)->isInstanceOf( Node::staticTypeId() ) ) + if( isReferenceable( child.get() ) ) { - toExport->add( *it ); - } - else if( const Plug *plug = IECore::runTimeCast( it->get() ) ) - { - if( - !boost::regex_match( plug->getName().c_str(), invisiblePlug ) - && plug != userPlug() - ) - { - toExport->add( *it ); - } + toExport->add( child.get() ); } } @@ -715,26 +701,13 @@ void SubGraph::loadReferenceInternal( const std::filesystem::path &fileName ) for( Plug::Iterator it( this ); !it.done(); ++it ) { Plug *plug = it->get(); - if( isReferencePlug( plug ) ) + if( isReferenceable( plug ) ) { previousPlugs[plug->getName()] = plug; plug->setName( "__tmp__" + plug->getName().string() ); } } - // We don't export user plugs to references, but old versions of - // Gaffer did, so as above, we must get them out of the way during - // the load. - for( Plug::Iterator it( userPlug() ); !it.done(); ++it ) - { - Plug *plug = it->get(); - if( isReferencePlug( plug ) ) - { - previousPlugs[plug->relativeName( this )] = plug; - plug->setName( "__tmp__" + plug->getName().string() ); - } - } - // if we're doing a reload, then we also need to delete all our child // nodes to make way for the incoming nodes. @@ -751,7 +724,6 @@ void SubGraph::loadReferenceInternal( const std::filesystem::path &fileName ) // Set up a container to catch all the children added during loading. StandardSetPtr newChildren = new StandardSet; childAddedSignal().connect( boost::bind( (bool (StandardSet::*)( IECore::RunTimeTypedPtr ) )&StandardSet::add, newChildren.get(), ::_2 ) ); - userPlug()->childAddedSignal().connect( boost::bind( (bool (StandardSet::*)( IECore::RunTimeTypedPtr ) )&StandardSet::add, newChildren.get(), ::_2 ) ); // load the reference. we use continueOnError=true to get everything possible // loaded, but if any errors do occur we throw an exception at the end of this @@ -870,51 +842,40 @@ bool SubGraph::isChildEdit( const Plug *plug ) const return m_referenceState && m_referenceState->plugEdits.isChildEdit( plug ); } -bool SubGraph::isReferencePlug( const Plug *plug ) const +bool SubGraph::isReferenceable( const GraphComponent *descendant ) const { - // If a plug is the descendant of a plug starting with - // __, and that plug is a direct child of the reference, - // assume that it is for gaffer's internal use, so would - // never come directly from a reference. This lines up - // with the export code in `exportReference()`, where - // such plugs are excluded from the export. - - // find ancestor of p which is a direct child of this node: - const Plug* ancestorPlug = plug; - const GraphComponent* parent = plug->parent(); - while( parent != this ) + // Walk up until `descendant` is immediately parented to us. + const GraphComponent *parent = descendant->parent(); + while( parent && parent != this ) { - ancestorPlug = runTimeCast< const Plug >( parent ); - if( !ancestorPlug ) - { - // Looks like the plug we're looking for doesn't exist, - // so we exit the loop. - break; - } - parent = ancestorPlug->parent(); + descendant = parent; + parent = descendant->parent(); } - if( ancestorPlug && boost::starts_with( ancestorPlug->getName().c_str(), "__" ) ) + if( !parent ) { + // We weren't an ancestor of `descendant`. return false; } - // we know this doesn't come from a reference, - // because it's made during construction. - if( plug == userPlug() ) + if( runTimeCast( descendant ) ) { - return false; + return true; } - - // User plugs are not meant to be referenced either. But old - // versions of Gaffer did export them so we must be careful. - // Since we make loaded plugs non-dynamic, we can assume that - // if the plug is dynamic it was added locally by a user - // rather than loaded from a reference. - if( ancestorPlug == userPlug() && plug->getFlags( Plug::Dynamic ) ) + else if( auto plug = runTimeCast( descendant ) ) { - return false; + // There are two classes of plug that we don't want to include + // in exported references : + // + // 1. The `user` plug and its children. We want the `user` namespace + // to be available to users of the reference, so it must be empty + // when exported. + // 2. Plugs prefixed with `__`. These are hidden plugs created by + // various UI components, for example to store the node's position + // in the GraphEditor. Arguably these should be metadata instead, + // but until they are, we need to ignore them. + return plug != userPlug() && !boost::starts_with( plug->getName().c_str(), "__" ); } - // everything else must be from a reference then. - return true; + + return false; } From c8b08136b1e55ccea1d22236029387fd8183ccc2 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Tue, 21 Apr 2026 11:36:32 +0100 Subject: [PATCH 7/7] BoxUI : Deduplicate path browsing code And prefer `pathlib` over ad-hoc string handling. --- python/GafferUI/BoxUI.py | 41 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/python/GafferUI/BoxUI.py b/python/GafferUI/BoxUI.py index 08ea829dba9..5f725d1b598 100644 --- a/python/GafferUI/BoxUI.py +++ b/python/GafferUI/BoxUI.py @@ -37,6 +37,7 @@ import os import inspect import functools +import pathlib import IECore @@ -214,6 +215,21 @@ def __formatPlugs( box, plugs ) : return result +def __waitForReferenceFileName( menu, node, **dialogueKeywords ) : + + bookmarks = GafferUI.Bookmarks.acquire( node, category="reference" ) + + path = Gaffer.FileSystemPath( bookmarks.getDefault( menu ) ) + path.setFilter( Gaffer.FileSystemPath.createStandardFilter( [ "grf" ] ) ) + + dialogue = GafferUI.PathChooserDialogue( path, leaf=True, bookmarks=bookmarks, **dialogueKeywords ) + path = dialogue.waitForPath( parentWindow = menu.ancestor( GafferUI.Window ) ) + + if not path : + return + + return pathlib.Path( path ).with_suffix( ".grf" ) + def __exportForReferencing( menu, node ) : nonDefaultPlugs = __nonDefaultPlugs( node ) @@ -239,34 +255,15 @@ def __exportForReferencing( menu, node ) : if not dialogue.waitForConfirmation() : return - bookmarks = GafferUI.Bookmarks.acquire( node, category="reference" ) - - path = Gaffer.FileSystemPath( bookmarks.getDefault( menu ) ) - path.setFilter( Gaffer.FileSystemPath.createStandardFilter( [ "grf" ] ) ) - - dialogue = GafferUI.PathChooserDialogue( path, title="Export reference", confirmLabel="Export", leaf=True, bookmarks=bookmarks ) - path = dialogue.waitForPath( parentWindow = menu.ancestor( GafferUI.Window ) ) - + path = __waitForReferenceFileName( menu, node, title="Export reference", confirmLabel="Export" ) if not path : return - path = str( path ) - if not path.endswith( ".grf" ) : - path += ".grf" - node.exportForReference( path ) def __importReference( menu, node ) : - bookmarks = GafferUI.Bookmarks.acquire( node, category="reference" ) - - path = Gaffer.FileSystemPath( bookmarks.getDefault( menu ) ) - path.setFilter( Gaffer.FileSystemPath.createStandardFilter( [ "grf" ] ) ) - - window = menu.ancestor( GafferUI.Window ) - dialogue = GafferUI.PathChooserDialogue( path, title="Import reference", confirmLabel="Import", leaf=True, valid=True, bookmarks=bookmarks ) - path = dialogue.waitForPath( parentWindow = window ) - + path = __waitForReferenceFileName( menu, node, title="Import reference", confirmLabel="Import", valid=True ) if not path : return @@ -274,7 +271,7 @@ def __importReference( menu, node ) : with GafferUI.ErrorDialogue.ErrorHandler( title = "Error Importing Reference", closeLabel = "Oy vey", - parentWindow = window + parentWindow = menu.ancestor( GafferUI.Window ) ) : with Gaffer.UndoScope( scriptNode ) : scriptNode.executeFile( str( path ), parent = node, continueOnError = True )