diff --git a/Changes.md b/Changes.md index 7d976019dd8..a7dd2bdcca4 100644 --- a/Changes.md +++ b/Changes.md @@ -21,6 +21,9 @@ API --- - ScenePathPlugValueWidget : The `scenePathPlugValueWidget:scene` metadata now accepts a space-separated list of plugs, taking the first plug which has an input connection. +- VisibleSet : Added Python constructor with keyword arguments for `expansions`, `inclusions` and `exclusions`. +- ScriptNodeAlgo : Added new namespace with functions for managing shared UI state for GafferSceneUI. +- ContextAlgo : Deprecated. Use ScriptNodeAlgo instead. Build ----- diff --git a/include/GafferSceneUI/ScriptNodeAlgo.h b/include/GafferSceneUI/ScriptNodeAlgo.h new file mode 100644 index 00000000000..13d57eab047 --- /dev/null +++ b/include/GafferSceneUI/ScriptNodeAlgo.h @@ -0,0 +1,117 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2024, Cinesite VFX Ltd. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above +// copyright notice, this list of conditions and the following +// disclaimer. +// +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided with +// the distribution. +// +// * Neither the name of John Haddon nor the names of +// any other contributors to this software may be used to endorse or +// promote products derived from this software without specific prior +// written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "GafferSceneUI/Export.h" + +#include "GafferScene/VisibleSet.h" + +#include "Gaffer/Signals.h" + +#include "IECore/PathMatcher.h" + +namespace Gaffer +{ + +class ScriptNode; + +} // namespace Gaffer + +namespace GafferScene +{ + +class ScenePlug; + +} // namespace GafferScene + +namespace GafferSceneUI +{ + +namespace ScriptNodeAlgo +{ + +using ChangedSignal = Gaffer::Signals::Signal>; + +/// Visible Set +/// =========== + +/// The UI components coordinate with each other to perform on-demand scene +/// generation by managing a VisibleSet specifying which scene locations should +/// be shown. For instance, this allows the Viewer to show the objects from +/// locations exposed by expansion performed in the HierarchyView, and vice +/// versa. +GAFFERSCENEUI_API void setVisibleSet( Gaffer::ScriptNode *script, const GafferScene::VisibleSet &visibleSet ); +GAFFERSCENEUI_API GafferScene::VisibleSet getVisibleSet( const Gaffer::ScriptNode *script ); +/// Returns a signal emitted when the visible set for `script` is changed. +GAFFERSCENEUI_API ChangedSignal &visibleSetChangedSignal( Gaffer::ScriptNode *script ); + +/// Visible Set Utilities +/// ===================== + +/// Appends paths to the current expansion, optionally adding all ancestor paths too. +GAFFERSCENEUI_API void expandInVisibleSet( Gaffer::ScriptNode *script, const IECore::PathMatcher &paths, bool expandAncestors = true ); +/// Appends descendant paths to the current expansion up to a specified maximum depth. +/// Returns a new PathMatcher containing the new leafs of this expansion. +GAFFERSCENEUI_API IECore::PathMatcher expandDescendantsInVisibleSet( Gaffer::ScriptNode *script, const IECore::PathMatcher &paths, const GafferScene::ScenePlug *scene, int depth = std::numeric_limits::max() ); + +/// Path Selection +/// ============== + +/// Similarly to Path Expansion, the UI components coordinate with each other to +/// perform scene selection, storing the paths to the currently selected +/// locations within the scene. +GAFFERSCENEUI_API void setSelectedPaths( Gaffer::ScriptNode *script, const IECore::PathMatcher &paths ); +GAFFERSCENEUI_API IECore::PathMatcher getSelectedPaths( const Gaffer::ScriptNode *script ); + +/// When multiple paths are selected, it can be useful to know which was the last path to be +/// added. Because `PathMatcher` is unordered, this must be specified separately. +/// +/// > Note : The last selected path is synchronised automatically with the list of selected +/// > paths. When `setLastSelectedPath()` is called, it adds the path to the main selection list. +/// > When `setSelectedPaths()` is called, an arbitrary path becomes the last selected path. +/// > +/// > Note : An empty path is considered to mean that there is no last selected path, _not_ +/// > that the scene root is selected. +GAFFERSCENEUI_API void setLastSelectedPath( Gaffer::ScriptNode *script, const std::vector &path ); +GAFFERSCENEUI_API std::vector getLastSelectedPath( const Gaffer::ScriptNode *script ); + +/// Returns a signal emitted when either the selected paths or last selected path change for `script`. +GAFFERSCENEUI_API ChangedSignal &selectedPathsChangedSignal( Gaffer::ScriptNode *script ); + +} // namespace ScriptNodeAlgo + +} // namespace GafferSceneUI diff --git a/python/GafferSceneTest/VisibleSetTest.py b/python/GafferSceneTest/VisibleSetTest.py index 3dc899cb2bb..7977796e67e 100644 --- a/python/GafferSceneTest/VisibleSetTest.py +++ b/python/GafferSceneTest/VisibleSetTest.py @@ -41,6 +41,32 @@ class VisibleSetTest( GafferSceneTest.SceneTestCase ) : + def testConstructor( self ) : + + vs = GafferScene.VisibleSet() + self.assertEqual( vs.expansions, IECore.PathMatcher() ) + self.assertEqual( vs.inclusions, IECore.PathMatcher() ) + self.assertEqual( vs.exclusions, IECore.PathMatcher() ) + + vs = GafferScene.VisibleSet( expansions = IECore.PathMatcher( [ "/e" ] ) ) + self.assertEqual( vs.expansions, IECore.PathMatcher( [ "/e" ] ) ) + self.assertEqual( vs.inclusions, IECore.PathMatcher() ) + self.assertEqual( vs.exclusions, IECore.PathMatcher() ) + + vs = GafferScene.VisibleSet( inclusions = IECore.PathMatcher( [ "/i" ] ) ) + self.assertEqual( vs.expansions, IECore.PathMatcher() ) + self.assertEqual( vs.inclusions, IECore.PathMatcher( [ "/i" ] ) ) + self.assertEqual( vs.exclusions, IECore.PathMatcher() ) + + vs = GafferScene.VisibleSet( + expansions = IECore.PathMatcher( [ "/e" ] ), + inclusions = IECore.PathMatcher( [ "/i" ] ), + exclusions = IECore.PathMatcher( [ "/x" ] ), + ) + self.assertEqual( vs.expansions, IECore.PathMatcher( [ "/e" ] ) ) + self.assertEqual( vs.inclusions, IECore.PathMatcher( [ "/i" ] ) ) + self.assertEqual( vs.exclusions, IECore.PathMatcher( [ "/x" ] ) ) + def testExpansions( self ) : e = GafferScene.VisibleSet() diff --git a/python/GafferSceneUITest/ScriptNodeAlgoTest.py b/python/GafferSceneUITest/ScriptNodeAlgoTest.py new file mode 100644 index 00000000000..c2cb0500f9c --- /dev/null +++ b/python/GafferSceneUITest/ScriptNodeAlgoTest.py @@ -0,0 +1,208 @@ +########################################################################## +# +# Copyright (c) 2024, Cinesite VFX Ltd. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of John Haddon nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specifiscript prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import IECore + +import Gaffer +import GafferTest +import GafferUITest +import GafferScene +import GafferSceneUI + +class ScriptNodeAlgoTest( GafferUITest.TestCase ) : + + def testSelectedPaths( self ) : + + script = Gaffer.ScriptNode() + + GafferSceneUI.ScriptNodeAlgo.setSelectedPaths( script, IECore.PathMatcher( [ "/" ] ) ) + self.assertEqual( GafferSceneUI.ScriptNodeAlgo.getSelectedPaths( script ), IECore.PathMatcher( [ "/" ] ) ) + + GafferSceneUI.ScriptNodeAlgo.setSelectedPaths( script, IECore.PathMatcher( [ "/", "/A" ] ) ) + self.assertEqual( GafferSceneUI.ScriptNodeAlgo.getSelectedPaths( script ), IECore.PathMatcher( [ "/", "/A" ] ) ) + + GafferSceneUI.ScriptNodeAlgo.setSelectedPaths( script, IECore.PathMatcher( [ "/", "/A", "/A/C" ] ) ) + self.assertEqual( GafferSceneUI.ScriptNodeAlgo.getSelectedPaths( script ), IECore.PathMatcher( [ "/", "/A", "/A/C" ] ) ) + + GafferSceneUI.ScriptNodeAlgo.setSelectedPaths( script, IECore.PathMatcher( [ "/A/C", "/A/B/D" ] ) ) + self.assertEqual( GafferSceneUI.ScriptNodeAlgo.getSelectedPaths( script ), IECore.PathMatcher( [ "/A/C", "/A/B/D" ] ) ) + + def testVisibleSet( self ) : + + script = Gaffer.ScriptNode() + + v = GafferScene.VisibleSet( expansions = IECore.PathMatcher( [ "/A" ] ) ) + GafferSceneUI.ScriptNodeAlgo.setVisibleSet( script, v ) + self.assertEqual( GafferSceneUI.ScriptNodeAlgo.getVisibleSet( script ), v ) + + def testSelectionIsCopied( self ) : + + script = Gaffer.ScriptNode() + + s = IECore.PathMatcher( [ "/a" ] ) + GafferSceneUI.ScriptNodeAlgo.setSelectedPaths( script, s ) + self.assertEqual( GafferSceneUI.ScriptNodeAlgo.getSelectedPaths( script ), s ) + + s.addPath( "/a/b" ) + self.assertNotEqual( GafferSceneUI.ScriptNodeAlgo.getSelectedPaths( script ), s ) + + s = GafferSceneUI.ScriptNodeAlgo.getSelectedPaths( script ) + self.assertEqual( GafferSceneUI.ScriptNodeAlgo.getSelectedPaths( script ), s ) + + s.addPath( "/a/b" ) + self.assertNotEqual( GafferSceneUI.ScriptNodeAlgo.getSelectedPaths( script ), s ) + + def testVisibleSetIsCopied( self ) : + + script = Gaffer.ScriptNode() + + v = GafferScene.VisibleSet( expansions = IECore.PathMatcher( [ "/a" ] ) ) + GafferSceneUI.ScriptNodeAlgo.setVisibleSet( script, v ) + self.assertEqual( GafferSceneUI.ScriptNodeAlgo.getVisibleSet( script ), v ) + + v.expansions.addPath( "/a/b" ) + self.assertNotEqual( GafferSceneUI.ScriptNodeAlgo.getVisibleSet( script ), v ) + + v = GafferSceneUI.ScriptNodeAlgo.getVisibleSet( script ) + self.assertEqual( GafferSceneUI.ScriptNodeAlgo.getVisibleSet( script ), v ) + + v.expansions.addPath( "/a/b" ) + self.assertNotEqual( GafferSceneUI.ScriptNodeAlgo.getVisibleSet( script ), v ) + + def testLastSelectedPath( self ) : + + script = Gaffer.ScriptNode() + self.assertEqual( GafferSceneUI.ScriptNodeAlgo.getLastSelectedPath( script ), "" ) + + s = IECore.PathMatcher( [ "/a", "/b" ] ) + GafferSceneUI.ScriptNodeAlgo.setSelectedPaths( script, s ) + self.assertTrue( s.match( GafferSceneUI.ScriptNodeAlgo.getLastSelectedPath( script ) ) & s.Result.ExactMatch ) + + GafferSceneUI.ScriptNodeAlgo.setLastSelectedPath( script, "/c" ) + self.assertEqual( GafferSceneUI.ScriptNodeAlgo.getLastSelectedPath( script ), "/c" ) + s = GafferSceneUI.ScriptNodeAlgo.getSelectedPaths( script ) + self.assertEqual( s, IECore.PathMatcher( [ "/a", "/b", "/c" ] ) ) + + GafferSceneUI.ScriptNodeAlgo.setSelectedPaths( script, IECore.PathMatcher() ) + self.assertEqual( GafferSceneUI.ScriptNodeAlgo.getLastSelectedPath( script ), "" ) + + def testSignals( self ) : + + script1 = Gaffer.ScriptNode() + script2 = Gaffer.ScriptNode() + + visibleSetCs1 = GafferTest.CapturingSlot( GafferSceneUI.ScriptNodeAlgo.visibleSetChangedSignal( script1 ) ) + visibleSetCs2 = GafferTest.CapturingSlot( GafferSceneUI.ScriptNodeAlgo.visibleSetChangedSignal( script2 ) ) + + # Modifying one thing should signal for only that thing. + + visibleSet = GafferScene.VisibleSet( inclusions = IECore.PathMatcher( [ "/A" ] ) ) + GafferSceneUI.ScriptNodeAlgo.setVisibleSet( script1, visibleSet ) + self.assertEqual( len( visibleSetCs1 ), 1 ) + self.assertEqual( len( visibleSetCs2 ), 0 ) + + # A no-op shouldn't signal. + + GafferSceneUI.ScriptNodeAlgo.setVisibleSet( script1, visibleSet ) + self.assertEqual( len( visibleSetCs1 ), 1 ) + self.assertEqual( len( visibleSetCs2 ), 0 ) + + def testVisibleSetExpansionUtilities( self ) : + + # A + # |__B + # |__D + # |__E + # |__C + # |__F + # |__G + + script = Gaffer.ScriptNode() + + script["G"] = GafferScene.Sphere() + script["G"]["name"].setValue( "G" ) + + script["F"] = GafferScene.Sphere() + script["F"]["name"].setValue( "F" ) + + script["D"] = GafferScene.Sphere() + script["D"]["name"].setValue( "D" ) + + script["E"] = GafferScene.Sphere() + script["E"]["name"].setValue( "E" ) + + script["C"] = GafferScene.Group() + script["C"]["name"].setValue( "C" ) + + script["C"]["in"][0].setInput( script["F"]["out"] ) + script["C"]["in"][1].setInput( script["G"]["out"] ) + + script["B"] = GafferScene.Group() + script["B"]["name"].setValue( "B" ) + + script["B"]["in"][0].setInput( script["D"]["out"] ) + script["B"]["in"][1].setInput( script["E"]["out"] ) + + A = GafferScene.Group() + A["name"].setValue( "A" ) + A["in"][0].setInput( script["B"]["out"] ) + A["in"][1].setInput( script["C"]["out"] ) + + GafferSceneUI.ScriptNodeAlgo.expandInVisibleSet( script, IECore.PathMatcher( [ "/A/B", "/A/C" ] ) ) + self.assertEqual( GafferSceneUI.ScriptNodeAlgo.getVisibleSet( script ).expansions, IECore.PathMatcher( [ "/", "/A", "/A/B", "/A/C" ] ) ) + + GafferSceneUI.ScriptNodeAlgo.setVisibleSet( script, GafferScene.VisibleSet() ) + GafferSceneUI.ScriptNodeAlgo.expandInVisibleSet( script, IECore.PathMatcher( [ "/A/B", "/A/C" ] ), expandAncestors = False ) + self.assertEqual( GafferSceneUI.ScriptNodeAlgo.getVisibleSet( script ).expansions, IECore.PathMatcher( [ "/A/B", "/A/C" ] ) ) + + GafferSceneUI.ScriptNodeAlgo.setVisibleSet( script, GafferScene.VisibleSet( expansions = IECore.PathMatcher( [ "/" ] ) ) ) + self.assertEqual( GafferSceneUI.ScriptNodeAlgo.getVisibleSet( script ).expansions, IECore.PathMatcher( [ "/" ] ) ) + newLeafs = GafferSceneUI.ScriptNodeAlgo.expandDescendantsInVisibleSet( script, IECore.PathMatcher( [ "/" ] ), A["out"] ) + self.assertEqual( GafferSceneUI.ScriptNodeAlgo.getVisibleSet( script ).expansions, IECore.PathMatcher( [ "/", "/A", "/A/B", "/A/C" ] ) ) + self.assertEqual( newLeafs, IECore.PathMatcher( [ "/A/B/D", "/A/B/E", "/A/C/G", "/A/C/F" ] ) ) + + GafferSceneUI.ScriptNodeAlgo.setVisibleSet( script, GafferScene.VisibleSet( expansions = IECore.PathMatcher( [ "/" ] ) ) ) + self.assertEqual( GafferSceneUI.ScriptNodeAlgo.getVisibleSet( script ).expansions, IECore.PathMatcher( [ "/" ] ) ) + newLeafs = GafferSceneUI.ScriptNodeAlgo.expandDescendantsInVisibleSet( script, IECore.PathMatcher( [ "/" ] ), A["out"], depth = 1 ) + self.assertEqual( GafferSceneUI.ScriptNodeAlgo.getVisibleSet( script ).expansions, IECore.PathMatcher( [ "/", "/A" ] ) ) + self.assertEqual( newLeafs, IECore.PathMatcher( [ "/A/B", "/A/C" ] ) ) + + newLeafs = GafferSceneUI.ScriptNodeAlgo.expandDescendantsInVisibleSet( script, IECore.PathMatcher( [ "/A/C" ] ), A["out"], depth = 1 ) + self.assertEqual( GafferSceneUI.ScriptNodeAlgo.getVisibleSet( script ).expansions, IECore.PathMatcher( [ "/", "/A", "/A/C" ] ) ) + self.assertEqual( newLeafs, IECore.PathMatcher( [ "/A/C/G", "/A/C/F" ] ) ) + +if __name__ == "__main__": + unittest.main() diff --git a/python/GafferSceneUITest/__init__.py b/python/GafferSceneUITest/__init__.py index 157aeaa6fe0..ee5fac8a19e 100644 --- a/python/GafferSceneUITest/__init__.py +++ b/python/GafferSceneUITest/__init__.py @@ -63,6 +63,7 @@ from .LightPositionToolTest import LightPositionToolTest from .RenderPassEditorTest import RenderPassEditorTest from .SelectionToolTest import SelectionToolTest +from .ScriptNodeAlgoTest import ScriptNodeAlgoTest if __name__ == "__main__": unittest.main() diff --git a/src/GafferSceneModule/VisibleSetBinding.cpp b/src/GafferSceneModule/VisibleSetBinding.cpp index ac324e1cef7..db0d5396423 100644 --- a/src/GafferSceneModule/VisibleSetBinding.cpp +++ b/src/GafferSceneModule/VisibleSetBinding.cpp @@ -53,6 +53,15 @@ using namespace GafferScene; namespace { +VisibleSet *constructor( const PathMatcher &expansions, const PathMatcher &inclusions, const PathMatcher &exclusions ) +{ + VisibleSet *result = new VisibleSet; + result->expansions = expansions; + result->inclusions = inclusions; + result->exclusions = exclusions; + return result; +} + std::string visibilityRepr( const VisibleSet::Visibility &visibility ) { const std::string drawMode = extract( object( visibility.drawMode ).attr( "__str__" )() ); @@ -77,7 +86,14 @@ void GafferSceneModule::bindVisibleSet() IECorePython::TypedDataFromType(); scope s = class_( "VisibleSet" ) - .def( init<>() ) + .def( "__init__", make_constructor( constructor, default_call_policies(), + ( + arg( "expansions" ) = PathMatcher(), + arg( "inclusions" ) = PathMatcher(), + arg( "exclusions" ) = PathMatcher() + ) + ) + ) .def( init() ) .def( "visibility", (VisibleSet::Visibility (VisibleSet ::*)( const std::vector &, const size_t ) const)&VisibleSet::visibility, arg( "minimumExpansionDepth" ) = 0 ) .def_readwrite( "expansions", &VisibleSet::expansions ) diff --git a/src/GafferSceneUI/ScriptNodeAlgo.cpp b/src/GafferSceneUI/ScriptNodeAlgo.cpp new file mode 100644 index 00000000000..bf401331704 --- /dev/null +++ b/src/GafferSceneUI/ScriptNodeAlgo.cpp @@ -0,0 +1,150 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2024, Cinesite VFX Ltd. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above +// copyright notice, this list of conditions and the following +// disclaimer. +// +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided with +// the distribution. +// +// * Neither the name of John Haddon nor the names of +// any other contributors to this software may be used to endorse or +// promote products derived from this software without specific prior +// written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +////////////////////////////////////////////////////////////////////////// + +#include "GafferSceneUI/ScriptNodeAlgo.h" + +#include "GafferSceneUI/ContextAlgo.h" + +#include "Gaffer/Context.h" +#include "Gaffer/ScriptNode.h" + +#include "boost/bind/bind.hpp" + +#include + +using namespace boost::placeholders; +using namespace Gaffer; +using namespace GafferSceneUI; + +namespace +{ + +struct ChangedSignals +{ + ScriptNodeAlgo::ChangedSignal visibleSetChangedSignal; + ScriptNodeAlgo::ChangedSignal selectedPathsChangedSignal; + Gaffer::Signals::ScopedConnection connection; +}; + +void contextChanged( IECore::InternedString variable, ScriptNode *script, ChangedSignals *signals ) +{ + if( ContextAlgo::affectsVisibleSet( variable ) ) + { + signals->visibleSetChangedSignal( script ); + } + + if( ContextAlgo::affectsSelectedPaths( variable ) || ContextAlgo::affectsLastSelectedPath( variable ) ) + { + signals->selectedPathsChangedSignal( script ); + } +} + +ChangedSignals &changedSignals( ScriptNode *script ) +{ + static std::unordered_map g_signals; + ChangedSignals &result = g_signals[script]; + if( !result.connection.connected() ) + { + // Either we just made the signals, or an old ScriptNode + // was destroyed and a new one made in its place. + result.connection = const_cast( script->context() )->changedSignal().connect( + boost::bind( &contextChanged, ::_2, script, &result ) + ); + } + return result; +} + +} // namespace + +/// Everything here is implemented as a shim on top of ContextAlgo. Our intention is to move everyone +/// over to using ScriptNodeAlgo, then to remove ContextAlgo and reimplement ScriptNodeAlgo using the +/// metadata API to store the state as metadata on the ScriptNode. This will bring several benefits : +/// +/// - We can drop all the special cases for ignoring `ui:` metadata in Contexts. Using the context +/// for UI state was a terrible idea in the first place. +/// - Copying contexts during compute will be cheaper, since there will be fewer variables. +/// - We'll be able to serialise the UI state with the script if we want to. + +void ScriptNodeAlgo::setVisibleSet( Gaffer::ScriptNode *script, const GafferScene::VisibleSet &visibleSet ) +{ + ContextAlgo::setVisibleSet( script->context(), visibleSet ); +} + +GafferScene::VisibleSet ScriptNodeAlgo::getVisibleSet( const Gaffer::ScriptNode *script ) +{ + return ContextAlgo::getVisibleSet( script->context() ); +} + +ScriptNodeAlgo::ChangedSignal &ScriptNodeAlgo::visibleSetChangedSignal( Gaffer::ScriptNode *script ) +{ + return changedSignals( script ).visibleSetChangedSignal; +} + +void ScriptNodeAlgo::expandInVisibleSet( Gaffer::ScriptNode *script, const IECore::PathMatcher &paths, bool expandAncestors ) +{ + ContextAlgo::expand( script->context(), paths, expandAncestors ); +} + +IECore::PathMatcher ScriptNodeAlgo::expandDescendantsInVisibleSet( Gaffer::ScriptNode *script, const IECore::PathMatcher &paths, const GafferScene::ScenePlug *scene, int depth ) +{ + return ContextAlgo::expandDescendants( script->context(), paths, scene, depth ); +} + +void ScriptNodeAlgo::setSelectedPaths( Gaffer::ScriptNode *script, const IECore::PathMatcher &paths ) +{ + ContextAlgo::setSelectedPaths( script->context(), paths ); +} + +IECore::PathMatcher ScriptNodeAlgo::getSelectedPaths( const Gaffer::ScriptNode *script ) +{ + return ContextAlgo::getSelectedPaths( script->context() ); +} + +void ScriptNodeAlgo::setLastSelectedPath( Gaffer::ScriptNode *script, const std::vector &path ) +{ + ContextAlgo::setLastSelectedPath( script->context(), path ); +} + +std::vector ScriptNodeAlgo::getLastSelectedPath( const Gaffer::ScriptNode *script ) +{ + return ContextAlgo::getLastSelectedPath( script->context() ); +} + +ScriptNodeAlgo::ChangedSignal &ScriptNodeAlgo::selectedPathsChangedSignal( Gaffer::ScriptNode *script ) +{ + return changedSignals( script ).selectedPathsChangedSignal; + +} diff --git a/src/GafferSceneUIModule/GafferSceneUIModule.cpp b/src/GafferSceneUIModule/GafferSceneUIModule.cpp index 77c6ad2f538..dafad06b03b 100644 --- a/src/GafferSceneUIModule/GafferSceneUIModule.cpp +++ b/src/GafferSceneUIModule/GafferSceneUIModule.cpp @@ -47,6 +47,7 @@ #include "QueryBinding.h" #include "SetEditorBinding.h" #include "RenderPassEditorBinding.h" +#include "ScriptNodeAlgoBinding.h" using namespace GafferSceneUIModule; @@ -64,5 +65,6 @@ BOOST_PYTHON_MODULE( _GafferSceneUI ) bindLightEditor(); bindSetEditor(); bindRenderPassEditor(); + bindScriptNodeAlgo(); } diff --git a/src/GafferSceneUIModule/ScriptNodeAlgoBinding.cpp b/src/GafferSceneUIModule/ScriptNodeAlgoBinding.cpp new file mode 100644 index 00000000000..6f0f79ddad7 --- /dev/null +++ b/src/GafferSceneUIModule/ScriptNodeAlgoBinding.cpp @@ -0,0 +1,120 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2024, Cinesite VFX Ltd. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above +// copyright notice, this list of conditions and the following +// disclaimer. +// +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided with +// the distribution. +// +// * Neither the name of John Haddon nor the names of +// any other contributors to this software may be used to endorse or +// promote products derived from this software without specific prior +// written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +////////////////////////////////////////////////////////////////////////// + +#include "boost/python.hpp" + +#include "ScriptNodeAlgoBinding.h" + +#include "GafferSceneUI/ScriptNodeAlgo.h" + +#include "GafferScene/ScenePlug.h" + +#include "Gaffer/ScriptNode.h" + +#include "IECorePython/ScopedGILRelease.h" + +using namespace boost::python; +using namespace IECore; +using namespace Gaffer; +using namespace GafferScene; +using namespace GafferSceneUI::ScriptNodeAlgo; + +namespace +{ + +void setVisibleSetWrapper( ScriptNode &script, const GafferScene::VisibleSet &visibleSet ) +{ + IECorePython::ScopedGILRelease gilRelease; + setVisibleSet( &script, visibleSet ); +} + +void expandInVisibleSetWrapper( ScriptNode &script, const IECore::PathMatcher &paths, bool expandAncestors ) +{ + IECorePython::ScopedGILRelease gilRelease; + expandInVisibleSet( &script, paths, expandAncestors ); +} + +PathMatcher expandDescendantsInVisibleSetWrapper( ScriptNode &script, PathMatcher &paths, ScenePlug &scene, int depth ) +{ + IECorePython::ScopedGILRelease gilRelease; + return expandDescendantsInVisibleSet( &script, paths, &scene, depth ); +} + +void setSelectedPathsWrapper( ScriptNode &script, const IECore::PathMatcher &paths ) +{ + IECorePython::ScopedGILRelease gilRelease; + setSelectedPaths( &script, paths ); +} + +void setLastSelectedPathWrapper( ScriptNode &script, const std::vector &path ) +{ + IECorePython::ScopedGILRelease gilRelease; + setLastSelectedPath( &script, path ); +} + +std::string getLastSelectedPathWrapper( const ScriptNode &script ) +{ + std::vector path = getLastSelectedPath( &script ); + if( path.empty() ) + { + return ""; + } + + std::string result; + ScenePlug::pathToString( path, result ); + return result; +} + +} // namespace + +void GafferSceneUIModule::bindScriptNodeAlgo() +{ + object module( borrowed( PyImport_AddModule( "GafferSceneUI.ScriptNodeAlgo" ) ) ); + scope().attr( "ScriptNodeAlgo" ) = module; + scope moduleScope( module ); + + def( "setVisibleSet", &setVisibleSetWrapper ); + def( "getVisibleSet", &getVisibleSet ); + def( "visibleSetChangedSignal", &visibleSetChangedSignal, return_value_policy() ); + + def( "expandInVisibleSet", &expandInVisibleSetWrapper, ( arg( "expandAncestors" ) = true ) ); + def( "expandDescendantsInVisibleSet", &expandDescendantsInVisibleSetWrapper, ( arg( "script" ), arg( "paths" ), arg( "scene" ), arg( "depth" ) = std::numeric_limits::max() ) ); + def( "setSelectedPaths", &setSelectedPathsWrapper ); + def( "getSelectedPaths", &getSelectedPaths ); + def( "setLastSelectedPath", &setLastSelectedPathWrapper ); + def( "getLastSelectedPath", &getLastSelectedPathWrapper ); + def( "selectedPathsChangedSignal", &selectedPathsChangedSignal, return_value_policy() ); +} diff --git a/src/GafferSceneUIModule/ScriptNodeAlgoBinding.h b/src/GafferSceneUIModule/ScriptNodeAlgoBinding.h new file mode 100644 index 00000000000..81d9b20cb86 --- /dev/null +++ b/src/GafferSceneUIModule/ScriptNodeAlgoBinding.h @@ -0,0 +1,44 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2024, Cinesite VFX Ltd. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above +// copyright notice, this list of conditions and the following +// disclaimer. +// +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided with +// the distribution. +// +// * Neither the name of John Haddon nor the names of +// any other contributors to this software may be used to endorse or +// promote products derived from this software without specific prior +// written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +////////////////////////////////////////////////////////////////////////// + +#pragma once + +namespace GafferSceneUIModule +{ + +void bindScriptNodeAlgo(); + +} // namespace GafferSceneUIModule