diff --git a/Changes.md b/Changes.md index 24d7d727d4..59007b2677 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/Box.h b/include/Gaffer/Box.h index f67c0937d6..c04cf6fa71 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/Reference.h b/include/Gaffer/Reference.h index 5181c9b2f6..23c97ce97f 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 1363d47c56..fd6e0df0a6 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,51 @@ 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. + + /// 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 ); + + /// 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; + + 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; @@ -63,14 +110,24 @@ 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; + private : + + void loadReferenceInternal( const std::filesystem::path &fileName ); + bool isReferenceable( const GraphComponent *descendant ) 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/python/Gaffer/__init__.py b/python/Gaffer/__init__.py index f4c9e884ce..1e5b76e6f9 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 bec1036d6d..7b44346abf 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/python/GafferUI/BoxUI.py b/python/GafferUI/BoxUI.py index 08ea829dba..5f725d1b59 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 ) diff --git a/src/Gaffer/Box.cpp b/src/Gaffer/Box.cpp index f8e0f9cc8c..1908a771d2 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/Reference.cpp b/src/Gaffer/Reference.cpp index 0cc3e391ff..566f1c94a0 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 9ace797b51..40bd641f8a 100644 --- a/src/Gaffer/SubGraph.cpp +++ b/src/Gaffer/SubGraph.cpp @@ -36,11 +36,476 @@ #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" +#include "boost/regex.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->isReferenceable( 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->isReferenceable( 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 +519,70 @@ 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" ); + } + + StandardSetPtr toExport = new StandardSet; + for( const auto &child : GraphComponent::Range( *this ) ) + { + if( isReferenceable( child.get() ) ) + { + toExport->add( child.get() ); + } + } + + 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" ); + 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() ) + ); +} + +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; +} + +SubGraph::ReferenceChangedSignal &SubGraph::referenceChangedSignal() +{ + return m_referenceChangedSignal; +} + void SubGraph::affects( const Plug *input, AffectedPlugsContainer &outputs ) const { DependencyNode::affects( input, outputs ); @@ -147,4 +676,206 @@ 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( isReferenceable( plug ) ) + { + previousPlugs[plug->getName()] = 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 ) ); + + // 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 ); + // 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 ); + // 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 ); + } + + // 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::isReferenceable( const GraphComponent *descendant ) const +{ + // Walk up until `descendant` is immediately parented to us. + const GraphComponent *parent = descendant->parent(); + while( parent && parent != this ) + { + descendant = parent; + parent = descendant->parent(); + } + + if( !parent ) + { + // We weren't an ancestor of `descendant`. + return false; + } + + if( runTimeCast( descendant ) ) + { + return true; + } + else if( auto plug = runTimeCast( descendant ) ) + { + // 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(), "__" ); + } + + return false; } diff --git a/src/GafferBindings/MetadataBinding.cpp b/src/GafferBindings/MetadataBinding.cpp index e4e8968a0e..654cf71e3c 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 d4b653ead6..6884874f32 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/GafferBindings/ValuePlugBinding.cpp b/src/GafferBindings/ValuePlugBinding.cpp index cafe28e9c2..920a9ccc80 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/SpreadsheetBinding.cpp b/src/GafferModule/SpreadsheetBinding.cpp index e3409bc42a..03d12ad447 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 f4f06b0b62..f59f002279 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 @@ -274,24 +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 load( Reference &r, const std::filesystem::path &f ) { IECorePython::ScopedGILRelease gilRelease; @@ -303,7 +337,19 @@ void load( Reference &r, const std::filesystem::path &f ) void GafferModule::bindSubGraph() { using SubGraphWrapper = DependencyNodeWrapper; - DependencyNodeClass(); + DependencyNodeClass() + .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 ) + .def( "isChildEdit", &SubGraph::isChildEdit ) + ; + + SignalClass, ReferenceChangedSlotCaller >( "ReferenceChangedSignal" ); + + Serialisation::registerSerialiser( SubGraph::staticTypeId(), new SubGraphSerialiser ); using BoxWrapper = DependencyNodeWrapper; @@ -313,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 ) @@ -337,14 +381,10 @@ 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" ); - Serialisation::registerSerialiser( Reference::staticTypeId(), new ReferenceSerialiser ); - NodeClass() .def( "setup", &setup, ( arg( "plug" ) ) ) .def( "acquireProcessor", &acquireProcessor, ( arg( "type" ), arg( "createIfNecessary" ) = true ) ) diff --git a/src/GafferModule/ValuePlugBinding.cpp b/src/GafferModule/ValuePlugBinding.cpp index 9cf00ddcf8..9f5ad8410a 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;