Skip to content

Commit

Permalink
Merge pull request #6036 from johnhaddon/scriptNodeAlgo14
Browse files Browse the repository at this point in the history
ScriptNodeAlgo : Add new namespace to eventually replace ContextAlgo
  • Loading branch information
murraystevenson authored Sep 20, 2024
2 parents 6613596 + 55a8ba7 commit 56d8197
Show file tree
Hide file tree
Showing 10 changed files with 688 additions and 1 deletion.
3 changes: 3 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----
Expand Down
117 changes: 117 additions & 0 deletions include/GafferSceneUI/ScriptNodeAlgo.h
Original file line number Diff line number Diff line change
@@ -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<void ( Gaffer::ScriptNode * ), Gaffer::Signals::CatchingCombiner<void>>;

/// 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<int>::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<IECore::InternedString> &path );
GAFFERSCENEUI_API std::vector<IECore::InternedString> 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
26 changes: 26 additions & 0 deletions python/GafferSceneTest/VisibleSetTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
208 changes: 208 additions & 0 deletions python/GafferSceneUITest/ScriptNodeAlgoTest.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions python/GafferSceneUITest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading

0 comments on commit 56d8197

Please sign in to comment.