diff --git a/python/GafferSceneUITest/OptionInspectorTest.py b/python/GafferSceneUITest/OptionInspectorTest.py index 1731ed067c7..43d3a1637c2 100644 --- a/python/GafferSceneUITest/OptionInspectorTest.py +++ b/python/GafferSceneUITest/OptionInspectorTest.py @@ -54,12 +54,12 @@ def testName( self ) : self.assertEqual( inspector.name(), "option:foo" ) @staticmethod - def __inspect( scene, optionName, editScope = None ) : + def __inspect( scene, optionName, editScope = None, context = Gaffer.Context() ) : editScopePlug = Gaffer.Plug() editScopePlug.setInput( editScope["enabled"] if editScope is not None else None ) inspector = GafferSceneUI.Private.OptionInspector( scene, editScopePlug, optionName ) - with Gaffer.Context() as context : + with context : return inspector.inspect() def __assertExpectedResult( @@ -425,5 +425,355 @@ def testReadOnlyMetadataSignalling( self ) : Gaffer.MetadataAlgo.setReadOnly( editScope, True ) self.assertEqual( len( cs ), 2 ) # Change affects the result of `inspect().editable()` + def testRenderPassValues( self ) : + + options = GafferScene.StandardOptions() + options["options"]["renderCamera"]["enabled"].setValue( True ) + options["options"]["renderCamera"]["value"].setValue( "/defaultCamera" ) + + spreadsheet = Gaffer.Spreadsheet() + spreadsheet["selector"].setValue( "${renderPass}" ) + spreadsheet["rows"].addColumn( options["options"]["renderCamera"]["value"] ) + options["options"]["renderCamera"]["value"].setInput( spreadsheet["out"][0] ) + + rowA = spreadsheet["rows"].addRow() + rowA["name"].setValue( "renderPassA" ) + rowA["cells"][0]["value"].setValue( "/cameraA" ) + + rowB = spreadsheet["rows"].addRow() + rowB["name"].setValue( "renderPassB" ) + rowB["cells"][0]["value"].setValue( "/cameraB" ) + + with Gaffer.Context() as context : + + self.assertEqual( + self.__inspect( options["out"], "render:camera", context = context ).value().value, + "/defaultCamera" + ) + + context["renderPass"] = "renderPassA" + self.assertEqual( + self.__inspect( options["out"], "render:camera", context = context ).value().value, + "/cameraA" + ) + + context["renderPass"] = "renderPassB" + self.assertEqual( + self.__inspect( options["out"], "render:camera", context = context ).value().value, + "/cameraB" + ) + + context["renderPass"] = "renderPassC" + self.assertEqual( + self.__inspect( options["out"], "render:camera", context = context ).value().value, + "/defaultCamera" + ) + + def testRenderPassSourceAndEdits( self ) : + + s = Gaffer.ScriptNode() + + s["standardOptions"] = GafferScene.StandardOptions() + s["standardOptions"]["options"]["renderCamera"]["enabled"].setValue( True ) + s["standardOptions"]["options"]["renderCamera"]["value"].setValue( "/defaultCamera" ) + + s["group"] = GafferScene.Group() + s["editScope1"] = Gaffer.EditScope() + s["editScope2"] = Gaffer.EditScope() + + s["group"]["in"][0].setInput( s["standardOptions"]["out"] ) + + s["editScope1"].setup( s["group"]["out"] ) + s["editScope1"]["in"].setInput( s["group"]["out"] ) + + s["editScope2"].setup( s["editScope1"]["out"] ) + s["editScope2"]["in"].setInput( s["editScope1"]["out"] ) + + with Gaffer.Context() as context : + + context["renderPass"] = "renderPassA" + + # Should be able to edit standardOptions directly. + + SourceType = GafferSceneUI.Private.Inspector.Result.SourceType + + self.__assertExpectedResult( + self.__inspect( s["group"]["out"], "render:camera", context = context ), + source = s["standardOptions"]["options"]["renderCamera"], + sourceType = SourceType.Other, + editable = True, + edit = s["standardOptions"]["options"]["renderCamera"] + ) + + # Even if there is an edit scope in the way + + self.__assertExpectedResult( + self.__inspect( s["editScope1"]["out"], "render:camera", context = context ), + source = s["standardOptions"]["options"]["renderCamera"], + sourceType = SourceType.Other, + editable = True, + edit = s["standardOptions"]["options"]["renderCamera"] + ) + + # We shouldn't be able to edit it if we've been told to use an EditScope and it isn't in the history + self.__assertExpectedResult( + self.__inspect( s["group"]["out"], "render:camera", s["editScope1"], context ), + source = s["standardOptions"]["options"]["renderCamera"], + sourceType = SourceType.Other, + editable = False, + nonEditableReason = "The target EditScope (editScope1) is not in the scene history." + ) + + # If it is in the history though, and we're told to use it, then we will. + + inspection = self.__inspect( s["editScope2"]["out"], "render:camera", s["editScope2"], context ) + self.assertIsNone( + GafferScene.EditScopeAlgo.acquireRenderPassOptionEdit( + s["editScope2"], "renderPassA", "render:camera", createIfNecessary = False + ) + ) + + self.__assertExpectedResult( + inspection, + source = s["standardOptions"]["options"]["renderCamera"], + sourceType = SourceType.Upstream, + editable = True + ) + + optionEditScope2Edit = inspection.acquireEdit() + self.assertIsNotNone( optionEditScope2Edit ) + self.assertEqual( + optionEditScope2Edit, + GafferScene.EditScopeAlgo.acquireRenderPassOptionEdit( + s["editScope2"], "renderPassA", "render:camera", createIfNecessary = False + ) + ) + + # If there's an edit downstream of the EditScope we're asked to use, + # then we're allowed to be editable still + + inspection = self.__inspect( s["editScope2"]["out"], "render:camera", s["editScope1"], context ) + self.assertTrue( inspection.editable() ) + self.assertEqual( inspection.nonEditableReason(), "" ) + self.assertEqual( + inspection.acquireEdit(), + GafferScene.EditScopeAlgo.acquireRenderPassOptionEdit( + s["editScope1"], "renderPassA", "render:camera", createIfNecessary = False + ) + ) + self.assertEqual( inspection.editWarning(), "" ) + + # If there is a source node inside an edit scope, make sure we use that + + s["editScope1"]["standardOptions2"] = GafferScene.StandardOptions() + s["editScope1"]["standardOptions2"]["options"]["resolutionMultiplier"]["enabled"].setValue( True ) + s["editScope1"]["standardOptions2"]["options"]["resolutionMultiplier"]["value"].setValue( 4.0 ) + s["editScope1"]["standardOptions2"]["in"].setInput( s["editScope1"]["BoxIn"]["out"] ) + s["editScope1"]["RenderPassOptionEdits"]["in"].setInput( s["editScope1"]["standardOptions2"]["out"] ) + + self.__assertExpectedResult( + self.__inspect( s["editScope2"]["out"], "render:resolutionMultiplier", s["editScope1"], context ), + source = s["editScope1"]["standardOptions2"]["options"]["resolutionMultiplier"], + sourceType = SourceType.EditScope, + editable = True, + edit = s["editScope1"]["standardOptions2"]["options"]["resolutionMultiplier"] + ) + + # If there is a OptionTweaks node in the scope's processor, make sure we use that + + cameraEdit = GafferScene.EditScopeAlgo.acquireRenderPassOptionEdit( + s["editScope1"], "renderPassA", "render:camera", createIfNecessary = True + ) + cameraEdit["enabled"].setValue( True ) + cameraEdit["value"].setValue( "/bar" ) + + self.__assertExpectedResult( + self.__inspect( s["editScope2"]["out"], "render:camera", s["editScope1"], context ), + source = cameraEdit, + sourceType = SourceType.EditScope, + editable = True, + edit = cameraEdit + ) + + # If there is a StandardOptions node downstream of the scope's scene processor, make sure we use that + + s["editScope1"]["standardOptions3"] = GafferScene.StandardOptions() + s["editScope1"]["standardOptions3"]["options"]["renderCamera"]["enabled"].setValue( True ) + s["editScope1"]["standardOptions3"]["options"]["renderCamera"]["value"].setValue( "/baz" ) + s["editScope1"]["standardOptions3"]["in"].setInput( s["editScope1"]["RenderPassOptionEdits"]["out"] ) + s["editScope1"]["BoxOut"]["in"].setInput( s["editScope1"]["standardOptions3"]["out"] ) + + self.__assertExpectedResult( + self.__inspect( s["editScope2"]["out"], "render:camera", s["editScope1"], context ), + source = s["editScope1"]["standardOptions3"]["options"]["renderCamera"], + sourceType = SourceType.EditScope, + editable = True, + edit = s["editScope1"]["standardOptions3"]["options"]["renderCamera"] + ) + + # If there is a StandardOptions node outside of an edit scope, make sure we use that with no scope + + s["independentOptions"] = GafferScene.StandardOptions() + s["independentOptions"]["options"]["renderCamera"]["enabled"].setValue( True ) + s["independentOptions"]["options"]["renderCamera"]["value"].setValue( "/camera" ) + s["independentOptions"]["in"].setInput( s["editScope2"]["out"] ) + + self.__assertExpectedResult( + self.__inspect( s["independentOptions"]["out"], "render:camera", None, context ), + source = s["independentOptions"]["options"]["renderCamera"], + sourceType = SourceType.Other, + editable = True, + edit = s["independentOptions"]["options"]["renderCamera"] + ) + + # Check editWarnings and nonEditableReasons + + self.__assertExpectedResult( + self.__inspect( s["independentOptions"]["out"], "render:camera", s["editScope2"], context ), + source = s["independentOptions"]["options"]["renderCamera"], + sourceType = SourceType.Downstream, + editable = True, + edit = optionEditScope2Edit, + editWarning = "Option has edits downstream in independentOptions." + ) + + s["editScope2"]["enabled"].setValue( False ) + + self.__assertExpectedResult( + self.__inspect( s["independentOptions"]["out"], "render:camera", s["editScope2"], context ), + source = s["independentOptions"]["options"]["renderCamera"], + sourceType = SourceType.Downstream, + editable = False, + nonEditableReason = "The target EditScope (editScope2) is disabled." + ) + + s["editScope2"]["enabled"].setValue( True ) + Gaffer.MetadataAlgo.setReadOnly( s["editScope2"], True ) + + self.__assertExpectedResult( + self.__inspect( s["editScope2"]["out"], "render:camera", s["editScope2"], context ), + source = s["editScope1"]["standardOptions3"]["options"]["renderCamera"], + sourceType = SourceType.Upstream, + editable = False, + nonEditableReason = "editScope2 is locked." + ) + + Gaffer.MetadataAlgo.setReadOnly( s["editScope2"], False ) + Gaffer.MetadataAlgo.setReadOnly( s["editScope2"]["RenderPassOptionEdits"]["edits"], True ) + + self.__assertExpectedResult( + self.__inspect( s["editScope2"]["out"], "render:camera", s["editScope2"], context ), + source = s["editScope1"]["standardOptions3"]["options"]["renderCamera"], + sourceType = SourceType.Upstream, + editable = False, + nonEditableReason = "editScope2.RenderPassOptionEdits.edits is locked." + ) + + Gaffer.MetadataAlgo.setReadOnly( s["editScope2"]["RenderPassOptionEdits"], True ) + self.__assertExpectedResult( + self.__inspect( s["editScope2"]["out"], "render:camera", s["editScope2"], context ), + source = s["editScope1"]["standardOptions3"]["options"]["renderCamera"], + sourceType = SourceType.Upstream, + editable = False, + nonEditableReason = "editScope2.RenderPassOptionEdits is locked." + ) + + def testMultipleRenderPassOptionEdits( self ) : + + s = Gaffer.ScriptNode() + + s["standardOptions"] = GafferScene.StandardOptions() + s["standardOptions"]["options"]["renderCamera"]["enabled"].setValue( True ) + s["standardOptions"]["options"]["renderCamera"]["value"].setValue( "/defaultCamera" ) + + s["customOptions"] = GafferScene.CustomOptions() + s["customOptions"]["in"].setInput( s["standardOptions"]["out"] ) + s["customOptions"]["options"].addChild( Gaffer.NameValuePlug( "renderPass:type", "default" ) ) + + s["editScope1"] = Gaffer.EditScope() + s["editScope2"] = Gaffer.EditScope() + + s["editScope1"].setup( s["customOptions"]["out"] ) + s["editScope1"]["in"].setInput( s["customOptions"]["out"] ) + + s["editScope2"].setup( s["editScope1"]["out"] ) + s["editScope2"]["in"].setInput( s["editScope1"]["out"] ) + + renderPasses = [ "renderPassA", "renderPassB", "gafferBot_beauty" ] + + def assertRenderPassEdits( renderPass, option, defaultValue ) : + + with Gaffer.Context() as context : + context["renderPass"] = renderPass + + inspection = self.__inspect( s["editScope1"]["out"], option, s["editScope1"], context ) + self.assertTrue( inspection.editable() ) + self.assertEqual( inspection.nonEditableReason(), "" ) + editScope1Edit = inspection.acquireEdit() + self.assertEqual( + editScope1Edit, + GafferScene.EditScopeAlgo.acquireRenderPassOptionEdit( + s["editScope1"], renderPass, option, createIfNecessary = False + ) + ) + self.assertEqual( inspection.editWarning(), "" ) + + self.assertEqual( + self.__inspect( s["editScope1"]["out"], option, context = context ).value().value, + defaultValue + ) + + editScope1Value = "/editScope1/{}".format( renderPass ) + editScope1Edit["enabled"].setValue( True ) + editScope1Edit["value"].setValue( editScope1Value ) + + inspection = self.__inspect( s["editScope2"]["out"], option, s["editScope2"], context ) + self.assertTrue( inspection.editable() ) + self.assertEqual( inspection.nonEditableReason(), "" ) + editScope2Edit = inspection.acquireEdit() + self.assertEqual( + editScope2Edit, + GafferScene.EditScopeAlgo.acquireRenderPassOptionEdit( + s["editScope2"], renderPass, option, createIfNecessary = False + ) + ) + self.assertEqual( inspection.editWarning(), "" ) + + self.assertEqual( + self.__inspect( s["editScope2"]["out"], option, context = context ).value().value, + editScope1Value + ) + + editScope2Edit["enabled"].setValue( True ) + editScope2Edit["value"].setValue( "/editScope2/{}".format( renderPass ) ) + + def assertRenderPassEditResults( renderPass, option ) : + + with Gaffer.Context() as context : + context["renderPass"] = renderPass + + self.assertEqual( + self.__inspect( s["editScope1"]["out"], option, context = context ).value().value, + "/editScope1/{}".format( renderPass ) + ) + + self.assertEqual( + self.__inspect( s["editScope2"]["out"], option, context = context ).value().value, + "/editScope2/{}".format( renderPass ) + ) + + for option, defaultValue in [ + ( "render:camera", "/defaultCamera" ), + ( "renderPass:type", "default" ), + ] : + for renderPass in renderPasses : + with self.subTest( renderPass = renderPass, option = option, defaultValue = defaultValue ) : + assertRenderPassEdits( renderPass, option, defaultValue ) + + for option in [ "render:camera", "renderPass:type" ] : + for renderPass in renderPasses : + with self.subTest( renderPass = renderPass, option = option ) : + assertRenderPassEditResults( renderPass, option ) + if __name__ == "__main__" : unittest.main() diff --git a/src/GafferSceneUI/OptionInspector.cpp b/src/GafferSceneUI/OptionInspector.cpp index a446bbcd78f..af5ca9c7a1b 100644 --- a/src/GafferSceneUI/OptionInspector.cpp +++ b/src/GafferSceneUI/OptionInspector.cpp @@ -60,6 +60,9 @@ using namespace GafferSceneUI::Private; namespace { +const std::string g_emptyString( "" ); +const std::string g_renderPassContextName( "renderPass" ); + // This uses the same strategy that ValuePlug uses for the hash cache, // using `plug->dirtyCount()` to invalidate previous cache entries when // a plug is dirtied. @@ -218,6 +221,8 @@ Gaffer::ValuePlugPtr OptionInspector::source( const GafferScene::SceneAlgo::Hist return nullptr; } + /// \todo Should we provide an `editWarning` here for render pass specific + /// edits that may affect other render passes? if( auto options = runTimeCast( sceneNode ) ) { for( const auto &plug : NameValuePlug::Range( *options->optionsPlug() ) ) @@ -247,34 +252,75 @@ Gaffer::ValuePlugPtr OptionInspector::source( const GafferScene::SceneAlgo::Hist Inspector::EditFunctionOrFailure OptionInspector::editFunction( Gaffer::EditScope *editScope, const GafferScene::SceneAlgo::History *history ) const { - const GraphComponent *readOnlyReason = EditScopeAlgo::optionEditReadOnlyReason( - editScope, - m_option - ); - - if( readOnlyReason ) + // If our history's context contains a non-empty `renderPass` variable, + // we'll want to make a specific edit for that render pass. + const std::string renderPass = history->context->get( g_renderPassContextName, g_emptyString ); + if( !renderPass.empty() ) { - // If we don't have an edit and the scope is locked, we error, - // as we can't add an edit. Other cases where we already _have_ - // an edit will have been found by `source()`. - return fmt::format( - "{} is locked.", - readOnlyReason->relativeName( readOnlyReason->ancestor() ) + const GraphComponent *readOnlyReason = EditScopeAlgo::renderPassOptionEditReadOnlyReason( + editScope, + renderPass, + m_option ); + + if( readOnlyReason ) + { + // If we don't have an edit and the scope is locked, we error, + // as we can't add an edit. Other cases where we already _have_ + // an edit will have been found by `source()`. + return fmt::format( + "{} is locked.", + readOnlyReason->relativeName( readOnlyReason->ancestor() ) + ); + } + else + { + return [ + editScope = EditScopePtr( editScope ), + renderPass, + option = m_option, + context = history->context + ] () { + Context::Scope scope( context.get() ); + return EditScopeAlgo::acquireRenderPassOptionEdit( + editScope.get(), + renderPass, + option + ); + }; + } } else { - return [ - editScope = EditScopePtr( editScope ), - option = m_option, - context = history->context - ] () { - Context::Scope scope( context.get() ); - return EditScopeAlgo::acquireOptionEdit( - editScope.get(), - option - ); - }; + const GraphComponent *readOnlyReason = EditScopeAlgo::optionEditReadOnlyReason( + editScope, + m_option + ); + + if( readOnlyReason ) + { + // If we don't have an edit and the scope is locked, we error, + // as we can't add an edit. Other cases where we already _have_ + // an edit will have been found by `source()`. + return fmt::format( + "{} is locked.", + readOnlyReason->relativeName( readOnlyReason->ancestor() ) + ); + } + else + { + return [ + editScope = EditScopePtr( editScope ), + option = m_option, + context = history->context + ] () { + Context::Scope scope( context.get() ); + return EditScopeAlgo::acquireOptionEdit( + editScope.get(), + option + ); + }; + } } }