From f2e031942f8b2bda2f66540299e3baf63d1552b5 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Mon, 26 Feb 2024 17:55:35 -0500 Subject: [PATCH 1/7] SelectionTool : Add `selectionModifierPlug` --- include/GafferSceneUI/SelectionTool.h | 7 ++++ python/GafferSceneUI/SelectionToolUI.py | 49 +++++++++++++++++++++++++ python/GafferSceneUI/TransformToolUI.py | 24 +----------- src/GafferSceneUI/SelectionTool.cpp | 18 +++++++++ 4 files changed, 75 insertions(+), 23 deletions(-) diff --git a/include/GafferSceneUI/SelectionTool.h b/include/GafferSceneUI/SelectionTool.h index 7451b217366..ffb3348e32f 100644 --- a/include/GafferSceneUI/SelectionTool.h +++ b/include/GafferSceneUI/SelectionTool.h @@ -42,6 +42,8 @@ #include "GafferUI/DragDropEvent.h" #include "GafferUI/Tool.h" +#include "Gaffer/StringPlug.h" + namespace GafferSceneUI { @@ -59,6 +61,9 @@ class GAFFERSCENEUI_API SelectionTool : public GafferUI::Tool GAFFER_NODE_DECLARE_TYPE( GafferSceneUI::SelectionTool, SelectionToolTypeId, GafferUI::Tool ); + Gaffer::StringPlug *selectModePlug(); + const Gaffer::StringPlug *selectModePlug() const; + private : static ToolDescription g_toolDescription; @@ -77,6 +82,8 @@ class GAFFERSCENEUI_API SelectionTool : public GafferUI::Tool bool m_acceptedButtonPress = false; bool m_initiatedDrag = false; + + static size_t g_firstPlugIndex; }; } // namespace GafferSceneUI diff --git a/python/GafferSceneUI/SelectionToolUI.py b/python/GafferSceneUI/SelectionToolUI.py index 047d3993c37..c2e5122dd99 100644 --- a/python/GafferSceneUI/SelectionToolUI.py +++ b/python/GafferSceneUI/SelectionToolUI.py @@ -34,7 +34,12 @@ # ########################################################################## +import imath + +import IECore + import Gaffer +import GafferUI import GafferSceneUI Gaffer.Metadata.registerNode( @@ -52,7 +57,51 @@ - 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", "GafferUI.PresetsPlugValueWidget", + + "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 ) ) diff --git a/python/GafferSceneUI/TransformToolUI.py b/python/GafferSceneUI/TransformToolUI.py index a0dbcefe20c..5bd5662ad01 100644 --- a/python/GafferSceneUI/TransformToolUI.py +++ b/python/GafferSceneUI/TransformToolUI.py @@ -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 ) : diff --git a/src/GafferSceneUI/SelectionTool.cpp b/src/GafferSceneUI/SelectionTool.cpp index 248b762b0d0..27f565e9f59 100644 --- a/src/GafferSceneUI/SelectionTool.cpp +++ b/src/GafferSceneUI/SelectionTool.cpp @@ -154,6 +154,10 @@ GAFFER_NODE_DEFINE_TYPE( SelectionTool ); SelectionTool::ToolDescription SelectionTool::g_toolDescription; static IECore::InternedString g_dragOverlayName( "__selectionToolDragOverlay" ); +size_t SelectionTool::g_firstPlugIndex = 0; + +const std::string g_noneSelectionModiferName = "None"; + SelectionTool::SelectionTool( SceneView *view, const std::string &name ) : Tool( view, name ) { @@ -165,12 +169,26 @@ SelectionTool::SelectionTool( SceneView *view, const std::string &name ) sg->dragEnterSignal().connect( boost::bind( &SelectionTool::dragEnter, this, ::_1, ::_2 ) ); sg->dragMoveSignal().connect( boost::bind( &SelectionTool::dragMove, this, ::_2 ) ); sg->dragEndSignal().connect( boost::bind( &SelectionTool::dragEnd, this, ::_2 ) ); + + storeIndexOfNextChild( g_firstPlugIndex ); + + addChild( new StringPlug( "selectMode", Plug::Direction::In, g_anySelectModeName ) ); } SelectionTool::~SelectionTool() { } +StringPlug *SelectionTool::selectModePlug() +{ + return getChild( g_firstPlugIndex ); +} + +const StringPlug *SelectionTool::selectModePlug() const +{ + return getChild( g_firstPlugIndex ); +} + SceneGadget *SelectionTool::sceneGadget() { return runTimeCast( view()->viewportGadget()->getPrimaryChild() ); From 1431f8ccc003fbac47c0f2dca4e8d3e1a87947bb Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Mon, 26 Feb 2024 17:56:21 -0500 Subject: [PATCH 2/7] SelectionTool : Selection modifier registration --- Changes.md | 5 + include/GafferSceneUI/SelectionTool.h | 15 +++ python/GafferSceneUI/SelectionToolUI.py | 7 ++ python/GafferSceneUITest/SelectionToolTest.py | 67 ++++++++++ python/GafferSceneUITest/__init__.py | 1 + src/GafferSceneUI/SelectionTool.cpp | 117 +++++++++++++++++- src/GafferSceneUIModule/ToolBinding.cpp | 45 ++++++- 7 files changed, 251 insertions(+), 6 deletions(-) create mode 100644 python/GafferSceneUITest/SelectionToolTest.py diff --git a/Changes.md b/Changes.md index 97b0e0a6ea7..5844e7216ba 100644 --- a/Changes.md +++ b/Changes.md @@ -414,6 +414,11 @@ Documentation - Node Reference : Removed duplicate entries for nodes that have been aliased by compatibility configs. +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.13.0 (relative to 1.3.12.0) ======== diff --git a/include/GafferSceneUI/SelectionTool.h b/include/GafferSceneUI/SelectionTool.h index ffb3348e32f..4e353284453 100644 --- a/include/GafferSceneUI/SelectionTool.h +++ b/include/GafferSceneUI/SelectionTool.h @@ -39,6 +39,8 @@ #include "GafferSceneUI/Export.h" #include "GafferSceneUI/TypeIds.h" +#include "GafferScene/ScenePlug.h" + #include "GafferUI/DragDropEvent.h" #include "GafferUI/Tool.h" @@ -64,6 +66,19 @@ class GAFFERSCENEUI_API SelectionTool : public GafferUI::Tool Gaffer::StringPlug *selectModePlug(); const Gaffer::StringPlug *selectModePlug() const; + using SelectFunction = std::function; + // 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 registeredSelectModes(); + static void deregisterSelectMode( const std::string &mode ); + private : static ToolDescription g_toolDescription; diff --git a/python/GafferSceneUI/SelectionToolUI.py b/python/GafferSceneUI/SelectionToolUI.py index c2e5122dd99..701938d70de 100644 --- a/python/GafferSceneUI/SelectionToolUI.py +++ b/python/GafferSceneUI/SelectionToolUI.py @@ -84,6 +84,13 @@ "plugValueWidget:type", "GafferUI.PresetsPlugValueWidget", + "presetNames", lambda plug : IECore.StringVectorData( + GafferSceneUI.SelectionTool.registeredSelectModeLabels() + ), + "presetValues", lambda plug : IECore.StringVectorData( + GafferSceneUI.SelectionTool.registeredSelectModes() + ), + "label", "Select", "toolbarLayout:section", "Bottom", diff --git a/python/GafferSceneUITest/SelectionToolTest.py b/python/GafferSceneUITest/SelectionToolTest.py new file mode 100644 index 00000000000..b5193f41ea1 --- /dev/null +++ b/python/GafferSceneUITest/SelectionToolTest.py @@ -0,0 +1,67 @@ +########################################################################## +# +# 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 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 tearDown( self ) : + + GafferUITest.TestCase.tearDown( self ) + + GafferSceneUI.SelectionTool.deregisterSelectMode( "testModifier" ) + GafferSceneUI.SelectionTool.deregisterSelectMode( "testModifier2" ) + + +if __name__ == "__main__" : + unittest.main() \ No newline at end of file diff --git a/python/GafferSceneUITest/__init__.py b/python/GafferSceneUITest/__init__.py index 70eeab0b4c9..157aeaa6fe0 100644 --- a/python/GafferSceneUITest/__init__.py +++ b/python/GafferSceneUITest/__init__.py @@ -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() diff --git a/src/GafferSceneUI/SelectionTool.cpp b/src/GafferSceneUI/SelectionTool.cpp index 27f565e9f59..12e893a8fe1 100644 --- a/src/GafferSceneUI/SelectionTool.cpp +++ b/src/GafferSceneUI/SelectionTool.cpp @@ -45,6 +45,10 @@ #include "GafferUI/Style.h" #include "boost/bind/bind.hpp" +#include "boost/multi_index/member.hpp" +#include "boost/multi_index/ordered_index.hpp" +#include "boost/multi_index/sequenced_index.hpp" +#include "boost/multi_index_container.hpp" using namespace boost::placeholders; using namespace Imath; @@ -54,6 +58,66 @@ using namespace GafferUI; using namespace GafferScene; using namespace GafferSceneUI; +namespace +{ + +using NamedSelectMode = std::pair; +using SelectModeMap = boost::multi_index::multi_index_container< + NamedSelectMode, + boost::multi_index::indexed_by< + boost::multi_index::ordered_unique< + boost::multi_index::member + >, + boost::multi_index::sequenced<> + > +>; + +const std::string g_standardSelectModeName = "/Standard"; + +SelectModeMap &selectModes() +{ + // Deliberately "leaking" map, as it may contain Python functors which + // cannot be destroyed during program exit (because Python will have been + // shut down first). + static auto g_selectModes = new SelectModeMap; + + if( g_selectModes->empty() ) + { + g_selectModes->insert( + { + g_standardSelectModeName, + []( const ScenePlug *scene, const ScenePlug::ScenePath &path ) + { + return path; + } + } + ); + } + return *g_selectModes; +} + +const GafferScene::ScenePlug::ScenePath modifyPath( + const std::string &modeName, + const ScenePlug *scene, + const GafferScene::ScenePlug::ScenePath &path +) +{ + if( path.empty() || modeName.empty() ) + { + return path; + } + + auto it = selectModes().find( modeName ); + if( it != selectModes().end() ) + { + return it->second( scene, path ); + } + + return path; +} + +} // namespace + ////////////////////////////////////////////////////////////////////////// // DragOverlay implementation ////////////////////////////////////////////////////////////////////////// @@ -156,8 +220,6 @@ static IECore::InternedString g_dragOverlayName( "__selectionToolDragOverlay" ); size_t SelectionTool::g_firstPlugIndex = 0; -const std::string g_noneSelectionModiferName = "None"; - SelectionTool::SelectionTool( SceneView *view, const std::string &name ) : Tool( view, name ) { @@ -172,7 +234,7 @@ SelectionTool::SelectionTool( SceneView *view, const std::string &name ) storeIndexOfNextChild( g_firstPlugIndex ); - addChild( new StringPlug( "selectMode", Plug::Direction::In, g_anySelectModeName ) ); + addChild( new StringPlug( "selectMode", Plug::Direction::In, g_standardSelectModeName ) ); } SelectionTool::~SelectionTool() @@ -194,6 +256,33 @@ SceneGadget *SelectionTool::sceneGadget() return runTimeCast( view()->viewportGadget()->getPrimaryChild() ); } +void SelectionTool::registerSelectMode( const std::string &name, SelectFunction function ) +{ + auto &m = selectModes(); + auto [it, inserted] = m.insert( { name, function } ); + + if( !inserted ) + { + m.replace( it, { name, function } ); + } +} + +std::vector SelectionTool::registeredSelectModes() +{ + std::vector result; + for( const auto &m : selectModes().get<1>() ) + { + result.push_back( m.first ); + } + + return result; +} + +void SelectionTool::deregisterSelectMode( const std::string &mode ) +{ + selectModes().erase( mode ); +} + SelectionTool::DragOverlay *SelectionTool::dragOverlay() { // All instances of SelectionTool share a single drag overlay - this @@ -227,6 +316,12 @@ bool SelectionTool::buttonPress( const GafferUI::ButtonEvent &event ) ScenePlug::ScenePath objectUnderMouse; sg->objectAt( event.line, objectUnderMouse ); + objectUnderMouse = modifyPath( + selectModePlug()->getValue(), + sceneGadget()->getScene(), + objectUnderMouse + ) ; + PathMatcher selection = sg->getSelection(); const bool shiftHeld = event.modifiers & ButtonEvent::Shift; @@ -357,13 +452,25 @@ bool SelectionTool::dragEnd( const GafferUI::DragDropEvent &event ) if( sg->objectsAt( dragOverlay()->getStartPosition(), dragOverlay()->getEndPosition(), inDragRegion ) ) { + PathMatcher inDragRegionTransformed; + const ScenePlug *scene = sceneGadget()->getScene(); + const std::string modeName = selectModePlug()->getValue(); + for( PathMatcher::Iterator it = inDragRegion.begin(), eIt = inDragRegion.end(); it != eIt; ++it ) + { + ScenePlug::ScenePath modifiedPath = modifyPath( modeName, scene, *it ); + if( modifiedPath.size() ) + { + inDragRegionTransformed.addPath( modifiedPath ); + } + } + if( event.modifiers & DragDropEvent::Control ) { - selection.removePaths( inDragRegion ); + selection.removePaths( inDragRegionTransformed ); } else { - selection.addPaths( inDragRegion ); + selection.addPaths( inDragRegionTransformed ); } ContextAlgo::setSelectedPaths( view()->getContext(), selection ); diff --git a/src/GafferSceneUIModule/ToolBinding.cpp b/src/GafferSceneUIModule/ToolBinding.cpp index 66c6610ef53..87bbeb44c21 100644 --- a/src/GafferSceneUIModule/ToolBinding.cpp +++ b/src/GafferSceneUIModule/ToolBinding.cpp @@ -186,12 +186,55 @@ object acquireTransformEdit( const TransformTool::Selection &s, bool createIfNec return p ? object( *p ) : object(); } +struct SelectModeHelper +{ + SelectModeHelper( object fn ) : m_fn( fn ) + { + } + + GafferScene::ScenePlug::ScenePath operator()( + const GafferScene::ScenePlug *scene, + const GafferScene::ScenePlug::ScenePath &path + ) + { + IECorePython::ScopedGILLock gilLock; + + try + { + const std::string pathString = GafferScene::ScenePlug::pathToString( path ); + GafferScene::ScenePlug::ScenePath newPath = extract( + m_fn( GafferScene::ScenePlugPtr( const_cast( scene ) ), pathString ) + ); + return newPath; + } + catch( const boost::python::error_already_set & ) + { + ExceptionAlgo::translatePythonException(); + } + } + private : + object m_fn; +}; + +void registerSelectMode( const std::string &modifierName, object modifier ) +{ + SelectModeHelper helper( modifier ); + SelectionTool::registerSelectMode( modifierName, helper ); +} + } // namespace void GafferSceneUIModule::bindTools() { - GafferBindings::NodeClass( nullptr, no_init ); + GafferBindings::NodeClass( nullptr, no_init ) + .def( "registerSelectMode", ®isterSelectMode, ( boost::python::arg( "modifierName" ), boost::python::arg( "modifier" ) ) ) + .staticmethod( "registerSelectMode" ) + .def( "registeredSelectModes", &SelectionTool::registeredSelectModes ) + .staticmethod( "registeredSelectModes" ) + .def( "deregisterSelectMode", &SelectionTool::deregisterSelectMode ) + .staticmethod( "deregisterSelectMode" ) + ; { GafferBindings::NodeClass( nullptr, no_init ) From 424a6d43d87469d80eb9f98d12efcf55da3a904b Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Tue, 27 Feb 2024 17:13:15 -0500 Subject: [PATCH 3/7] Startup : Register USD Kind selection modifiers --- Changes.md | 14 +++--- src/GafferSceneUI/SelectionTool.cpp | 15 ++++--- src/GafferSceneUIModule/ToolBinding.cpp | 57 ++++++++++++------------- startup/gui/usd.py | 54 +++++++++++++++++++++++ 4 files changed, 100 insertions(+), 40 deletions(-) diff --git a/Changes.md b/Changes.md index 5844e7216ba..f4f27f6d24e 100644 --- a/Changes.md +++ b/Changes.md @@ -6,6 +6,10 @@ 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, USD Kind modifiers are implemented. 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. + Improvements ------------ @@ -391,6 +395,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) ======== @@ -414,11 +423,6 @@ Documentation - Node Reference : Removed duplicate entries for nodes that have been aliased by compatibility configs. -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.13.0 (relative to 1.3.12.0) ======== diff --git a/src/GafferSceneUI/SelectionTool.cpp b/src/GafferSceneUI/SelectionTool.cpp index 12e893a8fe1..a151f17263c 100644 --- a/src/GafferSceneUI/SelectionTool.cpp +++ b/src/GafferSceneUI/SelectionTool.cpp @@ -316,11 +316,14 @@ bool SelectionTool::buttonPress( const GafferUI::ButtonEvent &event ) ScenePlug::ScenePath objectUnderMouse; sg->objectAt( event.line, objectUnderMouse ); - objectUnderMouse = modifyPath( - selectModePlug()->getValue(), - sceneGadget()->getScene(), - objectUnderMouse - ) ; + { + Context::Scope scopedContext( sg->getContext() ); + objectUnderMouse = modifyPath( + selectModePlug()->getValue(), + sceneGadget()->getScene(), + objectUnderMouse + ); + } PathMatcher selection = sg->getSelection(); @@ -455,6 +458,8 @@ bool SelectionTool::dragEnd( const GafferUI::DragDropEvent &event ) PathMatcher inDragRegionTransformed; const ScenePlug *scene = sceneGadget()->getScene(); const std::string modeName = selectModePlug()->getValue(); + + Context::Scope scopedContext( sg->getContext() ); for( PathMatcher::Iterator it = inDragRegion.begin(), eIt = inDragRegion.end(); it != eIt; ++it ) { ScenePlug::ScenePath modifiedPath = modifyPath( modeName, scene, *it ); diff --git a/src/GafferSceneUIModule/ToolBinding.cpp b/src/GafferSceneUIModule/ToolBinding.cpp index 87bbeb44c21..2872d7473ef 100644 --- a/src/GafferSceneUIModule/ToolBinding.cpp +++ b/src/GafferSceneUIModule/ToolBinding.cpp @@ -186,40 +186,37 @@ object acquireTransformEdit( const TransformTool::Selection &s, bool createIfNec return p ? object( *p ) : object(); } -struct SelectModeHelper +void registerSelectMode( const std::string &modifierName, object modifier ) { - SelectModeHelper( object fn ) : m_fn( fn ) - { - } - - GafferScene::ScenePlug::ScenePath operator()( - const GafferScene::ScenePlug *scene, - const GafferScene::ScenePlug::ScenePath &path - ) - { - IECorePython::ScopedGILLock gilLock; - - try - { - const std::string pathString = GafferScene::ScenePlug::pathToString( path ); - GafferScene::ScenePlug::ScenePath newPath = extract( - m_fn( GafferScene::ScenePlugPtr( const_cast( scene ) ), pathString ) - ); - return newPath; + auto selectModePtr = std::shared_ptr( + new boost::python::object( modifier ), + []( boost::python::object *o ) { + IECorePython::ScopedGILLock gilLock; + delete o; } - catch( const boost::python::error_already_set & ) + ); + + SelectionTool::registerSelectMode( + modifierName, + [selectModePtr]( + const GafferScene::ScenePlug *scene, + const GafferScene::ScenePlug::ScenePath &path + ) -> GafferScene::ScenePlug::ScenePath { - ExceptionAlgo::translatePythonException(); + IECorePython::ScopedGILLock gilLock; + try + { + const std::string pathString = GafferScene::ScenePlug::pathToString( path ); + return extract( + (*selectModePtr)( GafferScene::ScenePlugPtr( const_cast( scene ) ), pathString ) + ); + } + catch( const boost::python::error_already_set & ) + { + ExceptionAlgo::translatePythonException(); + } } - } - private : - object m_fn; -}; - -void registerSelectMode( const std::string &modifierName, object modifier ) -{ - SelectModeHelper helper( modifier ); - SelectionTool::registerSelectMode( modifierName, helper ); + ); } } // namespace diff --git a/startup/gui/usd.py b/startup/gui/usd.py index 2f1a6d04c85..bc3757a63e7 100644 --- a/startup/gui/usd.py +++ b/startup/gui/usd.py @@ -35,7 +35,15 @@ # ########################################################################## +import functools + +from pxr import Kind + +import IECore + import Gaffer +import GafferScene +import GafferSceneUI import GafferUSD # Default cone angle is 90 (an entire hemisphere), so replace with something @@ -45,3 +53,49 @@ # `texture:format == automatic` isn't well supported at present, so default # user-created lights to `latlong`. Gaffer.Metadata.registerValue( GafferUSD.USDLight, "parameters.texture:format", "userDefault", "latlong" ) + + +def __kindSelectionModifier( targetKind, scene, pathString ) : + path = pathString.split( "/" )[1:] + targetKind = targetKind[9:] # 9 = len( "USD Kind/" ) + + kind = None + while len( path ) > 0 : + attributes = scene.attributes( path ) + kind = attributes.get( "usd:kind", None ) + + if kind is not None and Kind.Registry.IsA( kind.value, targetKind ) : + break + path.pop() + + return path + + +usdKinds = Kind.Registry.GetAllKinds() + +# Build a simplified hierarchy for sorting +kindPaths = [] +for kind in usdKinds : + kindPath = kind + kindParent = Kind.Registry.GetBaseKind( kind ) + while kindParent != "" : + kindPath = kindParent + "/" + kindPath + kindParent = Kind.Registry.GetBaseKind( kindParent ) + kindPaths.append( kindPath ) + +kindPaths.sort( reverse = True) + +# We prefer to have "subcomponent" at the end. +try : + kindPaths.remove( "subcomponent" ) + kindPaths.append( "subcomponent" ) +except : + pass + +for kindPath in kindPaths : + # Add `USD Kind/` prefix so these entries go under that sub-heading. + kind = "USD Kind/" + kindPath.split( "/" )[-1] + GafferSceneUI.SelectionTool.registerSelectMode( + kind, + functools.partial( __kindSelectionModifier, kind ), + ) From 456ef67e64fcf5b594f0748f943c321ba38b8af4 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Thu, 7 Mar 2024 16:15:52 -0500 Subject: [PATCH 4/7] SelectionToolUI : Custom dropdown menu --- python/GafferSceneUI/SelectionToolUI.py | 79 ++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 8 deletions(-) diff --git a/python/GafferSceneUI/SelectionToolUI.py b/python/GafferSceneUI/SelectionToolUI.py index 701938d70de..8ffa68cc9e7 100644 --- a/python/GafferSceneUI/SelectionToolUI.py +++ b/python/GafferSceneUI/SelectionToolUI.py @@ -34,6 +34,7 @@ # ########################################################################## +import functools import imath import IECore @@ -82,14 +83,7 @@ which may differ from what is originally selected. """, - "plugValueWidget:type", "GafferUI.PresetsPlugValueWidget", - - "presetNames", lambda plug : IECore.StringVectorData( - GafferSceneUI.SelectionTool.registeredSelectModeLabels() - ), - "presetValues", lambda plug : IECore.StringVectorData( - GafferSceneUI.SelectionTool.registeredSelectModes() - ), + "plugValueWidget:type", "GafferSceneUI.SelectionToolUI.SelectModePlugValueWidget", "label", "Select", @@ -112,3 +106,72 @@ 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 ) From 513db883f3b2e9d4ba45aeed45c789ed7f3c4aa2 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Fri, 15 Mar 2024 15:10:51 -0400 Subject: [PATCH 5/7] SelectionToolUI : Fix size flicker on tool change Without this extra height, there is a jump in the UI when switching from `SelectionTool` to any `TransformTool` because `TransformToolUI._SelectionWidget` is taller and widgets are top-aligned. --- python/GafferSceneUI/TransformToolUI.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/GafferSceneUI/TransformToolUI.py b/python/GafferSceneUI/TransformToolUI.py index 5bd5662ad01..fb90667d0c5 100644 --- a/python/GafferSceneUI/TransformToolUI.py +++ b/python/GafferSceneUI/TransformToolUI.py @@ -115,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 From 387dc72986a306f469f12c6d3f20df2c17cbc4cc Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Thu, 21 Mar 2024 10:41:31 -0400 Subject: [PATCH 6/7] SelectionTool : Sync `selectMode` across tools --- include/GafferSceneUI/SelectionTool.h | 2 ++ python/GafferSceneUITest/SelectionToolTest.py | 25 +++++++++++++++++++ src/GafferSceneUI/SelectionTool.cpp | 14 +++++++++++ 3 files changed, 41 insertions(+) diff --git a/include/GafferSceneUI/SelectionTool.h b/include/GafferSceneUI/SelectionTool.h index 4e353284453..88e12e4c653 100644 --- a/include/GafferSceneUI/SelectionTool.h +++ b/include/GafferSceneUI/SelectionTool.h @@ -83,6 +83,8 @@ class GAFFERSCENEUI_API SelectionTool : public GafferUI::Tool static ToolDescription g_toolDescription; + void plugSet( Gaffer::Plug *plug ); + SceneGadget *sceneGadget(); class DragOverlay; diff --git a/python/GafferSceneUITest/SelectionToolTest.py b/python/GafferSceneUITest/SelectionToolTest.py index b5193f41ea1..df1476dfc4c 100644 --- a/python/GafferSceneUITest/SelectionToolTest.py +++ b/python/GafferSceneUITest/SelectionToolTest.py @@ -36,6 +36,8 @@ import unittest +import Gaffer +import GafferScene import GafferUITest import GafferSceneUI @@ -55,6 +57,29 @@ def testRegisterSelectMode( self ) : 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 ) diff --git a/src/GafferSceneUI/SelectionTool.cpp b/src/GafferSceneUI/SelectionTool.cpp index a151f17263c..aa3e99d2a62 100644 --- a/src/GafferSceneUI/SelectionTool.cpp +++ b/src/GafferSceneUI/SelectionTool.cpp @@ -232,6 +232,8 @@ SelectionTool::SelectionTool( SceneView *view, const std::string &name ) sg->dragMoveSignal().connect( boost::bind( &SelectionTool::dragMove, this, ::_2 ) ); sg->dragEndSignal().connect( boost::bind( &SelectionTool::dragEnd, this, ::_2 ) ); + plugSetSignal().connect( boost::bind( &SelectionTool::plugSet, this, ::_1 ) ); + storeIndexOfNextChild( g_firstPlugIndex ); addChild( new StringPlug( "selectMode", Plug::Direction::In, g_standardSelectModeName ) ); @@ -283,6 +285,18 @@ void SelectionTool::deregisterSelectMode( const std::string &mode ) selectModes().erase( mode ); } +void SelectionTool::plugSet( Plug *plug ) +{ + if( plug == selectModePlug() ) + { + const std::string value = selectModePlug()->getValue(); + for( auto &tool : SelectionTool::Range( *parent() ) ) + { + tool->selectModePlug()->setValue( value ); + } + } +} + SelectionTool::DragOverlay *SelectionTool::dragOverlay() { // All instances of SelectionTool share a single drag overlay - this From 08e17044374977b0456e47994637664324276ab5 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Thu, 21 Mar 2024 12:01:26 -0400 Subject: [PATCH 7/7] SelectionTool : Register `Shader Source` selector --- Changes.md | 4 +- startup/gui/selectionTool.py | 118 +++++++++++++++++++++++++++++++++++ startup/gui/usd.py | 54 ---------------- 3 files changed, 121 insertions(+), 55 deletions(-) create mode 100644 startup/gui/selectionTool.py diff --git a/Changes.md b/Changes.md index f4f27f6d24e..8d1efe668a4 100644 --- a/Changes.md +++ b/Changes.md @@ -8,7 +8,9 @@ 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, USD Kind modifiers are implemented. 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. +- 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 ------------ diff --git a/startup/gui/selectionTool.py b/startup/gui/selectionTool.py new file mode 100644 index 00000000000..5ea0d770359 --- /dev/null +++ b/startup/gui/selectionTool.py @@ -0,0 +1,118 @@ +########################################################################## +# +# Copyright (c) 2023, 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 functools +import IECoreScene + +from pxr import Kind + +import IECore + +import GafferSceneUI + +########################################################################## +# USD Kind +########################################################################## + +def __kindSelectionModifier( targetKind, scene, pathString ) : + path = pathString.split( "/" )[1:] + + kind = None + while len( path ) > 0 : + attributes = scene.attributes( path ) + kind = attributes.get( "usd:kind", None ) + + if kind is not None and Kind.Registry.IsA( kind.value, targetKind ) : + break + path.pop() + + return path + + +usdKinds = Kind.Registry.GetAllKinds() + +# Build a simplified hierarchy for sorting +kindPaths = [] +for kind in usdKinds : + kindPath = kind + kindParent = Kind.Registry.GetBaseKind( kind ) + while kindParent != "" : + kindPath = kindParent + "/" + kindPath + kindParent = Kind.Registry.GetBaseKind( kindParent ) + kindPaths.append( kindPath ) + +kindPaths.sort( reverse = True) + +# We prefer to have "subcomponent" at the end. +try : + kindPaths.remove( "subcomponent" ) + kindPaths.append( "subcomponent" ) +except : + pass + +for kindPath in kindPaths : + kind = kindPath.split( "/" )[-1] + GafferSceneUI.SelectionTool.registerSelectMode( + "USD Kind/" + IECore.CamelCase.toSpaced( kind ), + functools.partial( __kindSelectionModifier, kind ), + ) + +########################################################################## +# Shader Assignment +########################################################################## + +def __shaderSource( attributeKeyword, scene, pathString ) : + path = pathString.split( "/" )[1:] + + while len( path ) > 0 : + attributes = scene.attributes( path ) + for k, v in attributes.items() : + if ( + attributeKeyword in k.split( ':' ) and + k != "surface:full" and + k != "surface:preview" and + k != "displacement:full" and + k != "displacement:preview" and + isinstance( v, IECoreScene.ShaderNetwork ) + ) : + return path + + path.pop() + + return [] + +GafferSceneUI.SelectionTool.registerSelectMode( "Shader Assignment/Surface", functools.partial( __shaderSource, "surface" ) ) +GafferSceneUI.SelectionTool.registerSelectMode( "Shader Assignment/Displacement", functools.partial( __shaderSource, "displacement" ) ) diff --git a/startup/gui/usd.py b/startup/gui/usd.py index bc3757a63e7..2f1a6d04c85 100644 --- a/startup/gui/usd.py +++ b/startup/gui/usd.py @@ -35,15 +35,7 @@ # ########################################################################## -import functools - -from pxr import Kind - -import IECore - import Gaffer -import GafferScene -import GafferSceneUI import GafferUSD # Default cone angle is 90 (an entire hemisphere), so replace with something @@ -53,49 +45,3 @@ # `texture:format == automatic` isn't well supported at present, so default # user-created lights to `latlong`. Gaffer.Metadata.registerValue( GafferUSD.USDLight, "parameters.texture:format", "userDefault", "latlong" ) - - -def __kindSelectionModifier( targetKind, scene, pathString ) : - path = pathString.split( "/" )[1:] - targetKind = targetKind[9:] # 9 = len( "USD Kind/" ) - - kind = None - while len( path ) > 0 : - attributes = scene.attributes( path ) - kind = attributes.get( "usd:kind", None ) - - if kind is not None and Kind.Registry.IsA( kind.value, targetKind ) : - break - path.pop() - - return path - - -usdKinds = Kind.Registry.GetAllKinds() - -# Build a simplified hierarchy for sorting -kindPaths = [] -for kind in usdKinds : - kindPath = kind - kindParent = Kind.Registry.GetBaseKind( kind ) - while kindParent != "" : - kindPath = kindParent + "/" + kindPath - kindParent = Kind.Registry.GetBaseKind( kindParent ) - kindPaths.append( kindPath ) - -kindPaths.sort( reverse = True) - -# We prefer to have "subcomponent" at the end. -try : - kindPaths.remove( "subcomponent" ) - kindPaths.append( "subcomponent" ) -except : - pass - -for kindPath in kindPaths : - # Add `USD Kind/` prefix so these entries go under that sub-heading. - kind = "USD Kind/" + kindPath.split( "/" )[-1] - GafferSceneUI.SelectionTool.registerSelectMode( - kind, - functools.partial( __kindSelectionModifier, kind ), - )