Skip to content

Commit

Permalink
SetEditor : Display and filter selected set members
Browse files Browse the repository at this point in the history
  • Loading branch information
murraystevenson authored and johnhaddon committed Dec 12, 2023
1 parent d0221be commit 9a2a9f8
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 7 deletions.
3 changes: 3 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Improvements
------------

- 3Delight : Added support for subdivision corners and creases.
- SetEditor :
- Added "Selection" column displaying the number of currently selected members for each set.
- Added "Hide Empty Selection" checkbox. When on, the SetEditor will only display sets with currently selected members.

Fixes
-----
Expand Down
19 changes: 18 additions & 1 deletion python/GafferSceneUI/SetEditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,30 @@ def __init__( self, scriptNode, **kw ) :
emptySetFilter.userData()["UI"] = { "label" : "Hide Empty" }
emptySetFilter.setEnabled( False )

self.__filter = Gaffer.CompoundPathFilter( [ searchFilter, emptySetFilter ] )
emptySelectionFilter = _GafferSceneUI._SetEditor.EmptySetFilter( propertyName = "setPath:selectedMemberCount" )
emptySelectionFilter.userData()["UI"] = { "label" : "Hide Empty Selection" }
emptySelectionFilter.setEnabled( False )

self.__filter = Gaffer.CompoundPathFilter( [ searchFilter, emptySetFilter, emptySelectionFilter ] )

with mainColumn :

with GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) :

self.__searchFilterWidget = _SearchFilterWidget( searchFilter )
GafferUI.BasicPathFilterWidget( emptySetFilter )
GafferUI.BasicPathFilterWidget( emptySelectionFilter )

self.__setMembersColumn = GafferUI.StandardPathColumn( "Members", "setPath:memberCount" )
self.__selectedSetMembersColumn = GafferUI.StandardPathColumn( "Selection", "setPath:selectedMemberCount" )
self.__includedSetMembersColumn = _GafferSceneUI._SetEditor.VisibleSetInclusionsColumn( scriptNode.context() )
self.__excludedSetMembersColumn = _GafferSceneUI._SetEditor.VisibleSetExclusionsColumn( scriptNode.context() )
self.__pathListing = GafferUI.PathListingWidget(
Gaffer.DictPath( {}, "/" ), # temp till we make a SetPath
columns = [
_GafferSceneUI._SetEditor.SetNameColumn(),
self.__setMembersColumn,
self.__selectedSetMembersColumn,
self.__includedSetMembersColumn,
self.__excludedSetMembersColumn,
],
Expand Down Expand Up @@ -186,6 +193,8 @@ def __dragBegin( self, widget, event ) :
column = self.__pathListing.columnAt( imath.V2f( event.line.p0.x, event.line.p0.y ) )
if column == self.__setMembersColumn :
return IECore.StringVectorData( self.__getSetMembers( setNames ).paths() )
elif column == self.__selectedSetMembersColumn :
return IECore.StringVectorData( self.__getSelectedSetMembers( setNames ).paths() )
elif column == self.__includedSetMembersColumn :
return IECore.StringVectorData( self.__getIncludedSetMembers( setNames ).paths() )
elif column == self.__excludedSetMembersColumn :
Expand Down Expand Up @@ -264,6 +273,14 @@ def __getSetMembers( self, setNames, *unused ) :

return result

def __getSelectedSetMembers( self, setNames, *unused ) :

setMembers = self.__getSetMembers( setNames )
return IECore.PathMatcher( [
p for p in ContextAlgo.getSelectedPaths( self.getContext() ).paths()
if setMembers.match( p ) & ( IECore.PathMatcher.Result.ExactMatch | IECore.PathMatcher.Result.AncestorMatch )
] )

def __getIncludedSetMembers( self, setNames, *unused ) :

return self.__getSetMembers( setNames ).intersection( ContextAlgo.getVisibleSet( self.getContext() ).inclusions )
Expand Down
162 changes: 162 additions & 0 deletions python/GafferSceneUITest/SetEditorTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

import Gaffer
import GafferScene
import GafferSceneUI
import GafferUITest

from GafferSceneUI import _GafferSceneUI
Expand Down Expand Up @@ -182,6 +183,113 @@ def testSetPathMemberCount( self ) :
path.setFromString( parent )
self.assertEqual( path.property( "setPath:memberCount" ), count )

def testSetPathSelectedMemberCount( self ) :

plane = GafferScene.Plane()
plane["sets"].setValue( "A A:B A:C D E:F:G" )

planeB = GafferScene.Plane()
planeB["name"].setValue( "planeB" )
planeB["sets"].setValue( "A A:C D F" )

p = GafferScene.Parent()
p["parent"].setValue( "/" )
p["in"].setInput( plane["out"] )
p["children"]["child0"].setInput( planeB["out"] )

context = Gaffer.Context()
path = _GafferSceneUI._SetEditor.SetPath( p["out"], context, "/" )
self.assertTrue( path.isValid() )
self.assertFalse( path.isLeaf() )

for parent, selection, count in [
( "/", [], None ),
( "/", [ "/plane" ], None ),
( "/A", [ "/plane", "/planeB" ], 2 ),
( "/A", [ "/plane" ], 1 ),
( "/A", [ "/planeB" ], 1 ),
( "/A/A:B", [ "/plane" ], 1 ),
( "/A/A:B", [ "/planeB" ], 0 ),
( "/A/A:B", [], 0 ),
( "/A/A:C", [ "/plane", "/planeB" ], 2 ),
( "/A/A:C", [ "/plane" ], 1 ),
( "/A/A:C", [ "/planeB" ], 1 ),
( "/D", [ "/plane", "/planeB" ], 2 ),
( "/D", [ "/plane" ], 1 ),
( "/D", [ "/planeB" ], 1 ),
( "/E", [ "/plane", "/planeB" ], None ),
( "/E/F", [ "/plane" ], None ),
( "/E/F/E:F:G", [ "/plane", "/planeB" ], 1 ),
( "/E/F/E:F:G", [ "/plane" ], 1 ),
( "/E/F/E:F:G", [ "/planeB" ], 0 ),
( "/F", [ "/plane", "/planeB" ], 1 ),
( "/F", [ "/plane" ], 0 ),
( "/F", [ "/planeB" ], 1 ),
( "/A/A:D", [ "/plane", "/planeB" ], None ),
( "/D/D:A", [ "/planeB" ], None ),
] :

path.setFromString( parent )
GafferSceneUI.ContextAlgo.setSelectedPaths( context, IECore.PathMatcher( selection ) )
self.assertEqual( path.property( "setPath:selectedMemberCount" ), count )

def testSetPathSelectedMemberCountWithInheritance( self ) :

plane = GafferScene.Plane()
plane["sets"].setValue( "A" )

sphere = GafferScene.Sphere()

p = GafferScene.Parent()
p["parent"].setValue( "/plane" )
p["in"].setInput( plane["out"] )
p["children"]["child0"].setInput( sphere["out"] )

planeB = GafferScene.Plane()
planeB["name"].setValue( "planeB" )
planeB["sets"].setValue( "B" )

g = GafferScene.Group()
g["in"]["in0"].setInput( p["out"] )
g["in"]["in1"].setInput( planeB["out"] )

f = GafferScene.PathFilter()
f["paths"].setValue( IECore.StringVectorData( [ "/group" ] ) )

s = GafferScene.Set()
s["name"].setValue( "AB" )
s["in"].setInput( g["out"] )
s["filter"].setInput( f["out"] )

context = Gaffer.Context()
path = _GafferSceneUI._SetEditor.SetPath( s["out"], context, "/" )
self.assertTrue( path.isValid() )
self.assertFalse( path.isLeaf() )

for parent, selection, count in [
( "/", [], None ),
( "/", [ "/group" ], None ),
( "/", [ "/group/plane" ], None ),
( "/A", [ "/group/plane" ], 1 ),
( "/A", [ "/group/plane/sphere" ], 1 ),
( "/A", [ "/group/plane", "/group/plane/sphere" ], 2 ),
( "/A", [ "/group", "/group/plane", "/group/plane/sphere" ], 2 ),
( "/A", [ "/group" ], 0 ),
( "/A", [ "/group/planeB" ], 0 ),
( "/B", [ "/group/plane" ], 0 ),
( "/B", [ "/group" ], 0 ),
( "/B", [ "/group/planeB" ], 1 ),
( "/AB", [ "/group/plane" ], 1 ),
( "/AB", [ "/group" ], 1 ),
( "/AB", [ "/group/planeB" ], 1 ),
( "/AB", [ "/group", "/group/plane", "/group/planeB" ], 3 ),
( "/AB", [ "/group", "/group/plane", "/group/planeB", "/group/plane/sphere" ], 4 ),
] :

path.setFromString( parent )
GafferSceneUI.ContextAlgo.setSelectedPaths( context, IECore.PathMatcher( selection ) )
self.assertEqual( path.property( "setPath:selectedMemberCount" ), count )

def testSetPathCancellation( self ) :

plane = GafferScene.Plane()
Expand Down Expand Up @@ -267,3 +375,57 @@ def testEmptySetFilter( self ) :

emptySetFilter.setEnabled( True )
self.assertEqual( [ str( c ) for c in path.children() ], [ "/A/A:E" ] )

def testEmptySetFilterWithSelectedMemberCount( self ) :

plane = GafferScene.Plane()
plane["sets"].setValue( "A A:E B C D" )

planeB = GafferScene.Plane()
planeB["name"].setValue( "planeB" )
planeB["sets"].setValue( "A A:C D F" )

p = GafferScene.Parent()
p["parent"].setValue( "/" )
p["in"].setInput( plane["out"] )
p["children"]["child0"].setInput( planeB["out"] )

emptySet = GafferScene.Set()
emptySet["name"].setValue( "EMPTY A:EMPTY" )
emptySet["in"].setInput( p["out"] )

context = Gaffer.Context()
path = _GafferSceneUI._SetEditor.SetPath( emptySet["out"], context, "/" )

self.assertEqual( [ str( c ) for c in path.children() ], [ "/A", "/B", "/C", "/D", "/EMPTY", "/F" ] )

emptySetFilter = _GafferSceneUI._SetEditor.EmptySetFilter( propertyName = "setPath:selectedMemberCount" )
path.setFilter( emptySetFilter )

GafferSceneUI.ContextAlgo.setSelectedPaths( context, IECore.PathMatcher( [ "/plane" ] ) )
self.assertEqual( [ str( c ) for c in path.children() ], [ "/A", "/B", "/C", "/D" ] )

GafferSceneUI.ContextAlgo.setSelectedPaths( context, IECore.PathMatcher( [ "/planeB" ] ) )
self.assertEqual( [ str( c ) for c in path.children() ], [ "/A", "/D", "/F" ] )

GafferSceneUI.ContextAlgo.setSelectedPaths( context, IECore.PathMatcher( [ "/plane", "/planeB" ] ) )
self.assertEqual( [ str( c ) for c in path.children() ], [ "/A", "/B", "/C", "/D", "/F" ] )

GafferSceneUI.ContextAlgo.setSelectedPaths( context, IECore.PathMatcher( [] ) )
self.assertEqual( [ str( c ) for c in path.children() ], [] )

emptySetFilter.setEnabled( False )
self.assertEqual( [ str( c ) for c in path.children() ], [ "/A", "/B", "/C", "/D", "/EMPTY", "/F" ] )

path.setFromString( "/A" )
self.assertEqual( [ str( c ) for c in path.children() ], [ "/A/A:C", "/A/A:E", "/A/A:EMPTY" ] )

emptySetFilter.setEnabled( True )
GafferSceneUI.ContextAlgo.setSelectedPaths( context, IECore.PathMatcher( [ "/plane" ] ) )
self.assertEqual( [ str( c ) for c in path.children() ], [ "/A/A:E" ] )

GafferSceneUI.ContextAlgo.setSelectedPaths( context, IECore.PathMatcher( [ "/planeB" ] ) )
self.assertEqual( [ str( c ) for c in path.children() ], [ "/A/A:C" ] )

GafferSceneUI.ContextAlgo.setSelectedPaths( context, IECore.PathMatcher( [] ) )
self.assertEqual( [ str( c ) for c in path.children() ], [] )
41 changes: 35 additions & 6 deletions src/GafferSceneUIModule/SetEditorBinding.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ PathMatcherCache g_pathMatcherCache( pathMatcherCacheGetter, 25 );

const InternedString g_setNamePropertyName( "setPath:setName" );
const InternedString g_memberCountPropertyName( "setPath:memberCount" );
const InternedString g_selectedMemberCountPropertyName( "setPath:selectedMemberCount" );

//////////////////////////////////////////////////////////////////////////
// SetPath
Expand Down Expand Up @@ -252,6 +253,7 @@ class SetPath : public Gaffer::Path
Path::propertyNames( names, canceller );
names.push_back( g_setNamePropertyName );
names.push_back( g_memberCountPropertyName );
names.push_back( g_selectedMemberCountPropertyName );
}

IECore::ConstRunTimeTypedPtr property( const IECore::InternedString &name, const IECore::Canceller *canceller = nullptr ) const override
Expand All @@ -278,6 +280,29 @@ class SetPath : public Gaffer::Path
return new IntData( setMembers->readable().size() );
}
}
else if( name == g_selectedMemberCountPropertyName )
{
const PathMatcher p = pathMatcher( canceller );
if( p.match( names() ) & PathMatcher::ExactMatch )
{
Context::EditableScope scopedContext( getContext() );
if( canceller )
{
scopedContext.setCanceller( canceller );
}
const auto setMembers = getScene()->set( names().back().string() );
const auto selectedPaths = ContextAlgo::getSelectedPaths( Context::current() );
int memberCount = 0;
// Consider inheritance in selected member count so descendants
// of set members are included in the count
for( PathMatcher::Iterator it = setMembers->readable().begin(), eIt = setMembers->readable().end(); it != eIt; ++it )
{
memberCount += selectedPaths.subTree( *it ).size();
it.prune();
}
return new IntData( memberCount );
}
}
return Path::property( name, canceller );
}

Expand Down Expand Up @@ -915,7 +940,7 @@ class SetEditorSearchFilter : public Gaffer::PathFilter
};

//////////////////////////////////////////////////////////////////////////
// SetEditorEmptySetFilter - filters out paths that have a memberCount
// SetEditorEmptySetFilter - filters out paths that have a specified
// property value of 0. This also removes non-leaf paths if all their
// children have been removed by the filter.
//////////////////////////////////////////////////////////////////////////
Expand All @@ -927,8 +952,8 @@ class SetEditorEmptySetFilter : public Gaffer::PathFilter

IE_CORE_DECLAREMEMBERPTR( SetEditorEmptySetFilter )

SetEditorEmptySetFilter( IECore::CompoundDataPtr userData = nullptr )
: PathFilter( userData )
SetEditorEmptySetFilter( IECore::CompoundDataPtr userData = nullptr, const std::string &propertyName = g_memberCountPropertyName )
: PathFilter( userData ), m_propertyName( propertyName )
{
}

Expand Down Expand Up @@ -961,15 +986,19 @@ class SetEditorEmptySetFilter : public Gaffer::PathFilter
}

bool members = false;
if( const auto memberCountData = IECore::runTimeCast<const IECore::IntData>( path->property( g_memberCountPropertyName, canceller ) ) )
if( const auto memberCountData = IECore::runTimeCast<const IECore::IntData>( path->property( m_propertyName, canceller ) ) )
{
members = memberCountData->readable() > 0;
}

return leaf && !members;
}

};
private :

const InternedString m_propertyName;

};

} // namespace

Expand Down Expand Up @@ -1024,7 +1053,7 @@ void GafferSceneUIModule::bindSetEditor()
;

RefCountedClass<SetEditorEmptySetFilter, PathFilter>( "EmptySetFilter" )
.def( init<IECore::CompoundDataPtr>( ( boost::python::arg( "userData" ) = object() ) ) )
.def( init<IECore::CompoundDataPtr, const std::string &>( ( boost::python::arg( "userData" ) = object(), boost::python::arg( "propertyName" ) = g_memberCountPropertyName ) ) )
;

RefCountedClass<SetNameColumn, GafferUI::PathColumn>( "SetNameColumn" )
Expand Down

0 comments on commit 9a2a9f8

Please sign in to comment.