Skip to content

Commit

Permalink
Merge pull request #5720 from ericmehl/usdKindSelect
Browse files Browse the repository at this point in the history
Selection Modification
  • Loading branch information
johnhaddon authored Mar 28, 2024
2 parents cb895ad + 08e1704 commit fd2095f
Show file tree
Hide file tree
Showing 9 changed files with 554 additions and 27 deletions.
11 changes: 11 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ Fixes
1.4.0.0b5 (relative to 1.4.0.0b4)
=========

Features
--------
- SelectionTool : Added select mode plug. When set to anything except `Standard` using the SelectionTool causes the actual scene location selected to potentially be modified from the originally selected location. Selection modifiers work identically for deselection. Currently, two selectors are implemented :
- USD Kind : When selecting, the first ancestor location with a `usd:kind` attribute matching the chosen list of USD Kind will ultimately be selected. USD's Kind Registry includes `Assembly`, `Component`, `Group`, `Model` and `SubComponent` by default and can be extended via USD startup scripts.
- Shader Assignment : When selecting, the first ancestor location with a renderable and direct (not inherited) shader attribute will ultimately be selected. This can be used to select either surface or displacement shaders.

Improvements
------------

Expand Down Expand Up @@ -395,6 +401,11 @@ Fixes
- Instancer : Fixed handling of unindexed primvars in RootPerVertex mode.
- ArnoldShader : Fixed startup errors caused by unknown values in `widget` metadata.

API
---

- SelectionTool : Added static `registerSelectMode()` method for registering a Python or C++ function that will modify a selected scene path location. Users can choose which mode is active when selecting.

1.3.14.0 (relative to 1.3.13.1)
========

Expand Down
24 changes: 24 additions & 0 deletions include/GafferSceneUI/SelectionTool.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,13 @@
#include "GafferSceneUI/Export.h"
#include "GafferSceneUI/TypeIds.h"

#include "GafferScene/ScenePlug.h"

#include "GafferUI/DragDropEvent.h"
#include "GafferUI/Tool.h"

#include "Gaffer/StringPlug.h"

namespace GafferSceneUI
{

Expand All @@ -59,10 +63,28 @@ class GAFFERSCENEUI_API SelectionTool : public GafferUI::Tool

GAFFER_NODE_DECLARE_TYPE( GafferSceneUI::SelectionTool, SelectionToolTypeId, GafferUI::Tool );

Gaffer::StringPlug *selectModePlug();
const Gaffer::StringPlug *selectModePlug() const;

using SelectFunction = std::function<GafferScene::ScenePlug::ScenePath(
const GafferScene::ScenePlug *,
const GafferScene::ScenePlug::ScenePath &
)>;
// Registers a select mode identified by `name`. `function` must accept
// the scene from which a selection will be made and the `ScenePath` the user
// initially selected. It returns the `ScenePath` to use as the actual selection.
static void registerSelectMode( const std::string &name, SelectFunction function );
// Returns the names of registered modes, in the order they were registered.
// The "/Standard" mode will always be first.
static std::vector<std::string> registeredSelectModes();
static void deregisterSelectMode( const std::string &mode );

private :

static ToolDescription<SelectionTool, SceneView> g_toolDescription;

void plugSet( Gaffer::Plug *plug );

SceneGadget *sceneGadget();

class DragOverlay;
Expand All @@ -77,6 +99,8 @@ class GAFFERSCENEUI_API SelectionTool : public GafferUI::Tool

bool m_acceptedButtonPress = false;
bool m_initiatedDrag = false;

static size_t g_firstPlugIndex;
};

} // namespace GafferSceneUI
119 changes: 119 additions & 0 deletions python/GafferSceneUI/SelectionToolUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@
#
##########################################################################

import functools
import imath

import IECore

import Gaffer
import GafferUI
import GafferSceneUI

Gaffer.Metadata.registerNode(
Expand All @@ -52,7 +58,120 @@
- Drag to PathFilter or Set node to add/remove their paths
""",

"nodeToolbar:bottom:type", "GafferUI.StandardNodeToolbar.bottom",

"viewer:shortCut", "Q",
"order", 0,

# So we don't obscure the corner gnomon
"toolbarLayout:customWidget:LeftSpacer:widgetType", "GafferSceneUI.SelectionToolUI._LeftSpacer",
"toolbarLayout:customWidget:LeftSpacer:section", "Bottom",
"toolbarLayout:customWidget:LeftSpacer:index", 0,

# So our layout doesn't jump around too much when our selection widget changes size
"toolbarLayout:customWidget:RightSpacer:widgetType", "GafferSceneUI.SelectionToolUI._RightSpacer",
"toolbarLayout:customWidget:RightSpacer:section", "Bottom",
"toolbarLayout:customWidget:RightSpacer:index", -1,

plugs = {

"selectMode" : [

"description",
"""
Determines the scene location that is ultimately selected or deselected,
which may differ from what is originally selected.
""",

"plugValueWidget:type", "GafferSceneUI.SelectionToolUI.SelectModePlugValueWidget",

"label", "Select",

"toolbarLayout:section", "Bottom",
"toolbarLayout:width", 150,

],
},

)

class _LeftSpacer( GafferUI.Spacer ) :

def __init__( self, imageView, **kw ) :

GafferUI.Spacer.__init__( self, size = imath.V2i( 40, 1 ), maximumSize = imath.V2i( 40, 1 ) )

class _RightSpacer( GafferUI.Spacer ) :

def __init__( self, imageView, **kw ) :

GafferUI.Spacer.__init__( self, size = imath.V2i( 0, 0 ) )

class SelectModePlugValueWidget( GafferUI.PlugValueWidget ) :

def __init__( self, plugs, **kw ) :

self.__menuButton = GafferUI.MenuButton( "", menu = GafferUI.Menu( Gaffer.WeakMethod( self.__menuDefinition ) ) )

GafferUI.PlugValueWidget.__init__( self, self.__menuButton, plugs, **kw )

def _updateFromValues( self, values, exception ) :

if exception is not None :
self.__menuButton.setText( "" )
else :
modes = GafferSceneUI.SelectionTool.registeredSelectModes()

assert( len( values ) == 1 )

if values[0] in modes :
self.__menuButton.setText( values[0].partition( "/" )[-1] )
else :
self.__menuButton.setText( "Invalid" )

self.__menuButton.setErrored( exception is not None )

def _updateFromEditable( self ) :

self.__menuButton.setEnabled( self._editable() )

def __menuDefinition( self ) :

result = IECore.MenuDefinition()

modes = GafferSceneUI.SelectionTool.registeredSelectModes()

# dict mapping category names to the last inserted menu item for that category
# so we know where to insert the next item for the category.
modifiedCategories = {}

with self.getContext() :
currentValue = self.getPlug().getValue()

for mode in modes :
category, sep, label = mode.partition( "/" )

if category != "" and category not in modifiedCategories.keys() :
dividerPath = f"/__{category}Dividier"
result.append( dividerPath, { "divider" : True, "label" : category } )
modifiedCategories[category] = dividerPath

itemPath = f"/{label}"
itemDefinition = {
"command" : functools.partial( Gaffer.WeakMethod( self.__setValue ), mode ),
"checkBox" : mode == currentValue
}

if category in modifiedCategories.keys() :
result.insertAfter( itemPath, itemDefinition, modifiedCategories[category] )
else :
result.append( itemPath, itemDefinition )

modifiedCategories[category] = itemPath

return result

def __setValue( self, modifier, *unused ) :

with Gaffer.UndoScope( self.getPlug().ancestor( Gaffer.ScriptNode ) ) :
self.getPlug().setValue( modifier )
26 changes: 2 additions & 24 deletions python/GafferSceneUI/TransformToolUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,38 +59,16 @@
"toolbarLayout:customWidget:SelectionWidget:widgetType", "GafferSceneUI.TransformToolUI._SelectionWidget",
"toolbarLayout:customWidget:SelectionWidget:section", "Bottom",

# So we don't obscure the corner gnomon
"toolbarLayout:customWidget:LeftSpacer:widgetType", "GafferSceneUI.TransformToolUI._LeftSpacer",
"toolbarLayout:customWidget:LeftSpacer:section", "Bottom",
"toolbarLayout:customWidget:LeftSpacer:index", 0,

# So our layout doesn't jump around too much when our selection widget changes size
"toolbarLayout:customWidget:RightSpacer:widgetType", "GafferSceneUI.TransformToolUI._RightSpacer",
"toolbarLayout:customWidget:RightSpacer:section", "Bottom",
"toolbarLayout:customWidget:RightSpacer:index", -1,

"nodeToolbar:top:type", "GafferUI.StandardNodeToolbar.top",
"toolbarLayout:customWidget:TargetTipWidget:widgetType", "GafferSceneUI.TransformToolUI._TargetTipWidget",
"toolbarLayout:customWidget:TargetTipWidget:section", "Top",

"toolbarLayout:customWidget:TopRightSpacer:widgetType", "GafferSceneUI.TransformToolUI._RightSpacer",
"toolbarLayout:customWidget:TopRightSpacer:widgetType", "GafferSceneUI.SelectionToolUI._RightSpacer",
"toolbarLayout:customWidget:TopRightSpacer:section", "Top",
"toolbarLayout:customWidget:TopRightSpacer:index", -1,

)

class _LeftSpacer( GafferUI.Spacer ) :

def __init__( self, imageView, **kw ) :

GafferUI.Spacer.__init__( self, size = imath.V2i( 40, 1 ), maximumSize = imath.V2i( 40, 1 ) )

class _RightSpacer( GafferUI.Spacer ) :

def __init__( self, imageView, **kw ) :

GafferUI.Spacer.__init__( self, size = imath.V2i( 0, 0 ) )

def _boldFormatter( graphComponents ) :

with IECore.IgnoredExceptions( ValueError ) :
Expand Down Expand Up @@ -137,7 +115,7 @@ class _SelectionWidget( GafferUI.Frame ) :

def __init__( self, tool, **kw ) :

GafferUI.Frame.__init__( self, borderWidth = 4, **kw )
GafferUI.Frame.__init__( self, borderWidth = 1, **kw )

self.__tool = tool

Expand Down
92 changes: 92 additions & 0 deletions python/GafferSceneUITest/SelectionToolTest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
##########################################################################
#
# 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.
#
##########################################################################

import unittest

import Gaffer
import GafferScene
import GafferUITest
import GafferSceneUI

class SelectionToolTest( GafferUITest.TestCase ) :

def modifierFunction( scene, path ) :

return path

def testRegisterSelectMode( self ) :

GafferSceneUI.SelectionTool.registerSelectMode( "testModifier", self.modifierFunction )
GafferSceneUI.SelectionTool.registerSelectMode( "testModifier2", self.modifierFunction )

modifiers = GafferSceneUI.SelectionTool.registeredSelectModes()
self.assertEqual( len( modifiers ), 3 )

self.assertEqual( modifiers, [ "/Standard", "testModifier", "testModifier2" ] )

def testSyncSelectMode( self ) :

GafferSceneUI.SelectionTool.registerSelectMode( "testModifier", self.modifierFunction )

script = Gaffer.ScriptNode()
script["cube"] = GafferScene.Cube()

view = GafferSceneUI.SceneView()
view["in"].setInput( script["cube"]["out"] )

tool1 = GafferSceneUI.TranslateTool( view )
tool2 = GafferSceneUI.RotateTool( view )

self.assertEqual( len( [ i for i in view["tools"].children() if isinstance( i, GafferSceneUI.SelectionTool ) ] ), 2 )

tool1["selectMode"].setValue( "testModifier" )
self.assertEqual( tool1["selectMode"].getValue(), "testModifier" )
self.assertEqual( tool2["selectMode"].getValue(), "testModifier" )

tool2["selectMode"].setValue( "/Standard" )
self.assertEqual( tool1["selectMode"].getValue(), "/Standard" )
self.assertEqual( tool2["selectMode"].getValue(), "/Standard" )

def tearDown( self ) :

GafferUITest.TestCase.tearDown( self )

GafferSceneUI.SelectionTool.deregisterSelectMode( "testModifier" )
GafferSceneUI.SelectionTool.deregisterSelectMode( "testModifier2" )


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 @@ -62,6 +62,7 @@
from .OptionInspectorTest import OptionInspectorTest
from .LightPositionToolTest import LightPositionToolTest
from .RenderPassEditorTest import RenderPassEditorTest
from .SelectionToolTest import SelectionToolTest

if __name__ == "__main__":
unittest.main()
Loading

0 comments on commit fd2095f

Please sign in to comment.