From b85b1628ef342c8f1094444d9ae9c0f05b7ae211 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Fri, 25 Oct 2024 11:38:46 +0100 Subject: [PATCH 01/34] LightToolUI : Generalise description metadata The old description dated back to a time when only spotlights were supported. --- python/GafferSceneUI/LightToolUI.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/GafferSceneUI/LightToolUI.py b/python/GafferSceneUI/LightToolUI.py index 9d04d7362b9..5afbcf1bc99 100644 --- a/python/GafferSceneUI/LightToolUI.py +++ b/python/GafferSceneUI/LightToolUI.py @@ -49,7 +49,7 @@ "description", """ - Tool for editing spot lights. + Tool for editing light shapes, such as spot light cones or quad light width and height. """, "viewer:shortCut", "A", From 542e7f21589e2d24876cdf6d8b668a6b742a12e8 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Mon, 28 Oct 2024 15:33:14 +0000 Subject: [PATCH 02/34] CyclesShaderUI : Fix presets menus This was broken in #6082, where we switched the storage from Python dicts to CompoundData. Also moved the `label` variable closer to the point of usage and omitted the `flags` variable since it wasn't really helpful. The `label` variable actually worked without the `.value` part, but I added it to avoid any future surprises. --- Changes.md | 8 ++++++++ python/GafferCyclesUI/CyclesShaderUI.py | 9 ++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Changes.md b/Changes.md index 7c89d0d454a..d88974249a1 100644 --- a/Changes.md +++ b/Changes.md @@ -1,3 +1,11 @@ +1.5.x.x (relative to 1.5.0.0) +======= + +Fixes +----- + +- CyclesShader : Fixed broken presets menus. + 1.5.0.0 (relative to 1.4.15.0) ======= diff --git a/python/GafferCyclesUI/CyclesShaderUI.py b/python/GafferCyclesUI/CyclesShaderUI.py index b2152371989..95a41e6c13d 100644 --- a/python/GafferCyclesUI/CyclesShaderUI.py +++ b/python/GafferCyclesUI/CyclesShaderUI.py @@ -72,15 +72,13 @@ def __getSocketToComponents( socketType ) : def __translateParamMetadata( nodeTypeName, socketName, value ) : paramPath = nodeTypeName + ".parameters." + socketName - socketType = value["type"] - label = value["ui_name"] - flags = value["flags"] + socketType = value["type"].value if socketType == "enum" : presetNames = IECore.StringVectorData() presetValues = IECore.StringVectorData() for enumName, enumValues in value["enum_values"].items() : presetNames.append(enumName) - presetValues.append(enumValues) + presetValues.append( enumValues.value ) __metadata[paramPath]["presetNames"] = presetNames __metadata[paramPath]["presetValues"] = presetValues __metadata[paramPath]["plugValueWidget:type"] = "GafferUI.PresetsPlugValueWidget" @@ -94,10 +92,11 @@ def __translateParamMetadata( nodeTypeName, socketName, value ) : __metadata[paramPath]["fileSystemPath:extensionsLabel"] = "Show only image files" __metadata[paramPath]["noduleLayout:visible"] = True + label = value["ui_name"].value __metadata[paramPath]["label"] = label __metadata[paramPath]["noduleLayout:label"] = label # Linkable - linkable = bool( flags.value & ( 1 << 0 ) ) + linkable = bool( value["flags"].value & ( 1 << 0 ) ) __metadata[paramPath]["nodule:type"] = "" if not linkable else None # "" disables the nodule, and None falls through to the default if "category" in value : From ce17ae0bb19b8479d0636b44c43e4dc9e1bf65f0 Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Mon, 28 Oct 2024 22:13:58 +1100 Subject: [PATCH 03/34] README.md : Update for Gaffer 1.5 release Fixes #5260 --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 049bdab6a23..28df4f974fa 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Gaffer # -Gaffer is a VFX application that enables look developers, lighters, and compositors to easily build, tweak, iterate, and render scenes. Gaffer supports in-application scripting in Python and [OSL](https://github.com/imageworks/OpenShadingLanguage), so VFX artists and technical directors can design shaders, automate processes, and build production workflows. +Gaffer is a VFX application that enables look developers, lighters, and compositors to easily build, tweak, iterate, and render scenes. Gaffer supports in-application scripting in Python and [OSL](https://github.com/AcademySoftwareFoundation/OpenShadingLanguage), so VFX artists and technical directors can design shaders, automate processes, and build production workflows. An open-source project, Gaffer also provides an application framework for studios to design and create their own VFX production pipeline tools. Built using the [Cortex](https://github.com/ImageEngine/cortex) libraries, Gaffer ships with a multi-threaded, deferred evaluation engine and a flexible user interface framework. @@ -19,16 +19,16 @@ Participating in the Gaffer community requires abiding by the project's [Code of Compiled binary releases are available for download from the [releases page](https://github.com/GafferHQ/gaffer/releases). -Gaffer is officially supported and tested on **Linux** (CentOS 7) and **macOS** (macOS 10.14). +Gaffer is officially supported and tested on **Linux** (RHEL/Rocky/AlmaLinux 9) and **Windows** (Windows 10/11). ## Building ## [![CI](https://github.com/GafferHQ/gaffer/workflows/CI/badge.svg)](https://github.com/GafferHQ/gaffer/actions?query=workflow%3ACI) -Gaffer targets the [VFX Reference Platform](https://vfxplatform.com). We are currently on **CY2022**. Aside from general platform development packages, we specifically require the following tools that may not be installed by default on your system. Without these, you will not be able to build Gaffer. +Gaffer targets the [VFX Reference Platform](https://vfxplatform.com). We are currently on **CY2023**. Aside from general platform development packages, we specifically require the following tools that may not be installed by default on your system. Without these, you will not be able to build Gaffer. -> **Note:** From time to time, this list may change. For a complete, accurate, and up-to-date method of installing the prerequisites on CentOS, refer to the [Docker setup](https://github.com/GafferHQ/build/blob/master/Dockerfile) we use for building automatic releases. +> **Note:** From time to time, this list may change. For a complete, accurate, and up-to-date method of installing the prerequisites on Linux, refer to the [Docker setup](https://github.com/GafferHQ/build/blob/main/Dockerfile) we use for building automatic releases. ### Build requirements ### @@ -40,7 +40,7 @@ Gaffer targets the [VFX Reference Platform](https://vfxplatform.com). We are cur Package Name | Version ------------ |:--------------: -[`gcc`](https://gcc.gnu.org/index.html) | 6.3.1 +[`gcc`](https://gcc.gnu.org/index.html) | 11.2.1 [`scons`](http://www.scons.org) | [`inkscape`](http://inkscape.org) | @@ -51,13 +51,13 @@ Package Name | Version Package Name | Minimum Version ------------ |:--------------: -[`sphinx`](http://www.sphinx-doc.org/) | 1.8 +[`sphinx`](http://www.sphinx-doc.org/) | 4.3.1 Python Module | Required version ------------- |:---------------: -`sphinx_rtd_theme` | 0.4.3 -`recommonmark` | 0.5.0 -`docutils` | 0.12 +`sphinx_rtd_theme` | 1.0.0 +`myst-parser` | 0.15.2 +`docutils` | 0.17.1 ### Build process ### @@ -104,17 +104,17 @@ Gaffer dependencies ships with Cycles, but to build the modules for one of the o For example, the following command builds Gaffer with Arnold support: ```bash -scons build ARNOLD_ROOT=/path/to/arnold/6 BUILD_DIR=... +scons build ARNOLD_ROOT=/path/to/arnold BUILD_DIR=... ```` ## Questions and troubleshooting ## -If you have any questions about using Gaffer, or encounter problems setting it up, feel free to ask on the [Gaffer community group](https://groups.google.com/forum/#!forum/gaffer-dev). Our users and contributors are happy to help. +If you have any questions about using Gaffer, or encounter problems setting it up, feel free to ask on the [Gaffer community group](https://groups.google.com/g/gaffer-dev). Our users and contributors are happy to help. ## Requesting features ## -If there is a feature you would like to see in Gaffer, request it on the [Gaffer community group](https://groups.google.com/forum/#!forum/gaffer-dev). Do not create an Issue for it on GitHub. +If there is a feature you would like to see in Gaffer, request it on the [Gaffer community group](https://groups.google.com/g/gaffer-dev). Do not create an Issue for it on GitHub. ## Contributions and bugs reports ## @@ -124,10 +124,10 @@ Please see the project's [contribution guidelines](CONTRIBUTING.md). ## Copyright and license ## -© 2011–2020 John Haddon. All rights reserved. +© 2011–2024 John Haddon. All rights reserved. -© 2011–2020 Image Engine Design Inc. All rights reserved. +© 2011–2024 Image Engine Design Inc. All rights reserved. -© 2011–2020 Cinesite VFX Ltd. All rights reserved. +© 2011–2024 Cinesite VFX Ltd. All rights reserved. Distributed under the [BSD license](LICENSE). From 2e90fc7cd3dcf20a58903f7d3e284cf4654cad27 Mon Sep 17 00:00:00 2001 From: Brian R Hanke <59420805+BrianHanke@users.noreply.github.com> Date: Tue, 29 Oct 2024 18:00:54 -0400 Subject: [PATCH 04/34] gaffer.cmd : Add CORTEX_STARTUP_PATHS Add CORTEX_STARTUP_PATHS to fix the Cycles/color/float/swizzle issue discussed on Discord. --- Changes.md | 3 +++ bin/gaffer.cmd | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Changes.md b/Changes.md index d88974249a1..fbf54984a23 100644 --- a/Changes.md +++ b/Changes.md @@ -5,6 +5,9 @@ Fixes ----- - CyclesShader : Fixed broken presets menus. +- Windows : + - Fixed handling of shader colour component to float connections in Cycles. + - Added `CORTEX_STARTUP_PATHS` to match the Linux wrapper. 1.5.0.0 (relative to 1.4.15.0) ======= diff --git a/bin/gaffer.cmd b/bin/gaffer.cmd index 02ee3d81956..7de2be18fe2 100644 --- a/bin/gaffer.cmd +++ b/bin/gaffer.cmd @@ -35,6 +35,9 @@ call :prependToPath "%USERPROFILE%\gaffer\apps;%GAFFER_ROOT%\apps" GAFFER_APP_PA call :prependToPath "%USERPROFILE%\gaffer\startup" GAFFER_STARTUP_PATHS call :appendToPath "%GAFFER_ROOT%\startup" GAFFER_STARTUP_PATHS +call :prependToPath "%USERPROFILE%\gaffer\startup" CORTEX_STARTUP_PATHS +call :appendToPath "%GAFFER_ROOT%\startup" CORTEX_STARTUP_PATHS + call :prependToPath "%GAFFER_ROOT%\graphics" GAFFERUI_IMAGE_PATHS set OSLHOME=%GAFFER_ROOT% From d70acf705f16d6e544cad121d117cd00cc5fbfe7 Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:32:51 +1100 Subject: [PATCH 05/34] PlugPopup : Fix error when popups have no PlugValueWidget This could occur when a popup was created from multiple plugs of differing type, where we'd display a message rather than a PlugValueWidget. --- Changes.md | 3 +++ python/GafferUI/PlugPopup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Changes.md b/Changes.md index cb89f26a766..f950c65bee5 100644 --- a/Changes.md +++ b/Changes.md @@ -1,7 +1,10 @@ 1.4.x.x (relative to 1.4.15.0) ======= +Fixes +----- +- PlugPopup : Fixed error when displaying a popup with no PlugValueWidget. 1.4.15.0 (relative to 1.4.14.0) ======== diff --git a/python/GafferUI/PlugPopup.py b/python/GafferUI/PlugPopup.py index d948264e789..83ae19d9364 100644 --- a/python/GafferUI/PlugPopup.py +++ b/python/GafferUI/PlugPopup.py @@ -157,7 +157,7 @@ def __visibilityChanged( self, unused ) : def __focusChanged( self, oldWidget, newWidget ) : - if self.__plugValueWidget.isAncestorOf( newWidget ) and hasattr( newWidget, "activatedSignal" ) : + if self.__plugValueWidget and self.__plugValueWidget.isAncestorOf( newWidget ) and hasattr( newWidget, "activatedSignal" ) : self.__widgetActivatedConnection = newWidget.activatedSignal().connect( Gaffer.WeakMethod( self.__activated ), scoped = True ) From 57a21be6d635e654ec98e529dd90a476a85f06fd Mon Sep 17 00:00:00 2001 From: John Haddon Date: Thu, 31 Oct 2024 11:05:16 +0000 Subject: [PATCH 06/34] Bump version to 1.4.15.1 --- Changes.md | 9 +++++++-- SConstruct | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Changes.md b/Changes.md index f950c65bee5..9b969194603 100644 --- a/Changes.md +++ b/Changes.md @@ -1,5 +1,10 @@ -1.4.x.x (relative to 1.4.15.0) -======= +1.4.15.x (relative to 1.4.15.1) +======== + + + +1.4.15.1 (relative to 1.4.15.0) +======== Fixes ----- diff --git a/SConstruct b/SConstruct index 65d49b57fbd..67fd47c3129 100644 --- a/SConstruct +++ b/SConstruct @@ -64,7 +64,7 @@ if codecs.lookup( locale.getpreferredencoding() ).name != "utf-8" : gafferMilestoneVersion = 1 # for announcing major milestones - may contain all of the below gafferMajorVersion = 4 # backwards-incompatible changes gafferMinorVersion = 15 # new backwards-compatible features -gafferPatchVersion = 0 # bug fixes +gafferPatchVersion = 1 # bug fixes gafferVersionSuffix = "" # used for alpha/beta releases : "a1", "b2", etc. # All of the following must be considered when determining From 27d6529a80ea746563a7a5b762e4c8dc7c6e8af7 Mon Sep 17 00:00:00 2001 From: Daniel Dresser Date: Wed, 30 Oct 2024 19:48:03 -0700 Subject: [PATCH 07/34] Instancer : Fix issue with wrong prototypes in encapsulated render --- Changes.md | 1 + src/GafferScene/Instancer.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/Changes.md b/Changes.md index be700a54ad1..0878746fa62 100644 --- a/Changes.md +++ b/Changes.md @@ -9,6 +9,7 @@ Fixes - Fixed handling of shader colour component to float connections in Cycles. - Added `CORTEX_STARTUP_PATHS` to match the Linux wrapper. - PlugPopup : Fixed error when displaying a popup with no PlugValueWidget. +- Instancer : Fixed issue where wrong prototypes were sometimes used in encapsulated renders. 1.5.0.0 (relative to 1.4.15.0) ======= diff --git a/src/GafferScene/Instancer.cpp b/src/GafferScene/Instancer.cpp index a6f6647160c..97ed413e554 100644 --- a/src/GafferScene/Instancer.cpp +++ b/src/GafferScene/Instancer.cpp @@ -2711,6 +2711,7 @@ struct Prototype : public IECore::RefCounted IECore::MurmurHash h = hash; h.append( prototypeContext->hash() ); + h.append( *prototypeRoot ); // We find the capsules using the engine at shutter open, but the time used to construct the capsules // must be the on-frame time, since the capsules will add their own shutter From 70968dd12c02453511773da1dd3223215a0475c9 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Thu, 31 Oct 2024 11:20:28 +0000 Subject: [PATCH 08/34] Bump version to 1.5.0.1 --- Changes.md | 7 ++++++- SConstruct | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Changes.md b/Changes.md index 0878746fa62..5217cbb8811 100644 --- a/Changes.md +++ b/Changes.md @@ -1,4 +1,9 @@ -1.5.x.x (relative to 1.5.0.0) +1.5.x.x (relative to 1.5.0.1) +======= + + + +1.5.0.1 (relative to 1.5.0.0) ======= Fixes diff --git a/SConstruct b/SConstruct index ab5b90166b5..2e85847cc6d 100644 --- a/SConstruct +++ b/SConstruct @@ -64,7 +64,7 @@ if codecs.lookup( locale.getpreferredencoding() ).name != "utf-8" : gafferMilestoneVersion = 1 # for announcing major milestones - may contain all of the below gafferMajorVersion = 5 # backwards-incompatible changes gafferMinorVersion = 0 # new backwards-compatible features -gafferPatchVersion = 0 # bug fixes +gafferPatchVersion = 1 # bug fixes gafferVersionSuffix = "" # used for alpha/beta releases : "a1", "b2", etc. # All of the following must be considered when determining From 3a7b341c98a052f529f17fd801185ff52607addc Mon Sep 17 00:00:00 2001 From: Daniel Dresser Date: Thu, 10 Oct 2024 19:23:25 -0700 Subject: [PATCH 09/34] GafferTest::TestRunner : Accept multiple categories to exclude This matches the existing documentation in `gaffer test -help` --- python/GafferTest/TestRunner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/GafferTest/TestRunner.py b/python/GafferTest/TestRunner.py index 7ed1ad0c889..28c3f9b4666 100644 --- a/python/GafferTest/TestRunner.py +++ b/python/GafferTest/TestRunner.py @@ -190,12 +190,12 @@ def filterCategories( test, inclusions = "*", exclusions = "" ) : testMethod = getattr( test, test._testMethodName ) ## \todo Remove `standard` fallback (breaking change). categories = getattr( testMethod, "categories", { "standard" } ) - if not any( IECore.StringAlgo.match( c, inclusions ) for c in categories ) : + if not any( IECore.StringAlgo.matchMultiple( c, inclusions ) for c in categories ) : setattr( test, test._testMethodName, unittest.skip( f"Categories not included by `{inclusions}`" )( testMethod ) ) - elif any( IECore.StringAlgo.match( c, exclusions ) for c in categories ) : + elif any( IECore.StringAlgo.matchMultiple( c, exclusions ) for c in categories ) : setattr( test, test._testMethodName, unittest.skip( f"Categories excluded by `{exclusions}`" )( testMethod ) From b29be3f543683a61b0cc6c12e29741c4406af686 Mon Sep 17 00:00:00 2001 From: Daniel Dresser Date: Thu, 10 Oct 2024 17:51:51 -0700 Subject: [PATCH 10/34] Instancer : Support Int64VectorData for ids. --- Changes.md | 3 + python/GafferSceneTest/InstancerTest.py | 80 ++++++++++++-- src/GafferScene/Instancer.cpp | 139 +++++++++++++++++------- 3 files changed, 172 insertions(+), 50 deletions(-) diff --git a/Changes.md b/Changes.md index 5217cbb8811..f2d97dd7d4a 100644 --- a/Changes.md +++ b/Changes.md @@ -1,7 +1,10 @@ 1.5.x.x (relative to 1.5.0.1) ======= +Improvements +------------ +- Instancer : Added support for 64 bit ints for ids ( matching what is loaded from USD ). 1.5.0.1 (relative to 1.5.0.0) ======= diff --git a/python/GafferSceneTest/InstancerTest.py b/python/GafferSceneTest/InstancerTest.py index 818fad3dea3..6e70822c760 100644 --- a/python/GafferSceneTest/InstancerTest.py +++ b/python/GafferSceneTest/InstancerTest.py @@ -1498,11 +1498,17 @@ def testSetsWithDeepPrototypeRoots( self ) : ) def testIds( self ) : + with self.subTest( useInt64 = False ): + self.runTestIds( False ) + with self.subTest( useInt64 = True ): + self.runTestIds( True ) + + def runTestIds( self, useInt64 ) : points = IECoreScene.PointsPrimitive( IECore.V3fVectorData( [ imath.V3f( x, 0, 0 ) for x in range( 0, 4 ) ] ) ) points["id"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, - IECore.IntVectorData( [ 10, 100, 111, 5 ] ), + ( IECore.Int64VectorData if useInt64 else IECore.IntVectorData)( [ 10, 100, 111, 5 ] ), ) points["index"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, @@ -1560,16 +1566,16 @@ def testIds( self ) : self.assertEncapsulatedRendersSame( instancer ) - def testNegativeIdsAndIndices( self ) : + def testExtremeIdsAndIndices( self ) : - points = IECoreScene.PointsPrimitive( IECore.V3fVectorData( [ imath.V3f( x, 0, 0 ) for x in range( 0, 2 ) ] ) ) + points = IECoreScene.PointsPrimitive( IECore.V3fVectorData( [ imath.V3f( x, 0, 0 ) for x in range( 0, 4 ) ] ) ) points["id"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, - IECore.IntVectorData( [ -10, -5 ] ), + IECore.Int64VectorData( [ -10, -5, 8000000000, 8000000001 ] ), ) points["index"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, - IECore.IntVectorData( [ -1, -2 ] ), + IECore.IntVectorData( [ -1, -2, 1, 0 ] ), ) objectToScene = GafferScene.ObjectToScene() @@ -1582,24 +1588,57 @@ def testNegativeIdsAndIndices( self ) : instances["children"][0].setInput( cube["out"] ) instances["parent"].setValue( "/" ) + allFilter = GafferScene.PathFilter() + allFilter["paths"].setValue( IECore.StringVectorData( [ '/*' ] ) ) + + customAttributes = GafferScene.CustomAttributes() + customAttributes["in"].setInput( instances["out"] ) + customAttributes["filter"].setInput( allFilter["out"] ) + customAttributes["attributes"].addChild( Gaffer.NameValuePlug( "intAttr", Gaffer.IntPlug( "value", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ), True, "member1" ) ) + + customAttributes["ReadContextExpression"] = Gaffer.Expression() + customAttributes["ReadContextExpression"].setExpression( + 'parent["attributes"]["member1"]["value"] = context.get( "seed", -1 )' + ) + instancer = GafferScene.Instancer() instancer["in"].setInput( objectToScene["out"] ) - instancer["prototypes"].setInput( instances["out"] ) - instancer["parent"].setValue( "/object" ) + instancer["prototypes"].setInput( customAttributes["out"] ) + instancer["filter"].setInput( allFilter["out"] ) instancer["prototypeIndex"].setValue( "index" ) instancer["id"].setValue( "id" ) + instancer["seedEnabled"].setValue( True ) + instancer["rawSeed"].setValue( True ) self.assertEqual( instancer["out"].childNames( "/object/instances" ), IECore.InternedStringVectorData( [ "sphere", "cube" ] ) ) - self.assertEqual( instancer["out"].childNames( "/object/instances/sphere" ), IECore.InternedStringVectorData( [ "-5" ] ) ) - self.assertEqual( instancer["out"].childNames( "/object/instances/cube" ), IECore.InternedStringVectorData( [ "-10" ] ) ) + self.assertEqual( instancer["out"].childNames( "/object/instances/sphere" ), IECore.InternedStringVectorData( [ "-5", "8000000001" ] ) ) + self.assertEqual( instancer["out"].childNames( "/object/instances/cube" ), IECore.InternedStringVectorData( [ "-10", "8000000000" ] ) ) self.assertEqual( instancer["out"].childNames( "/object/instances/sphere/-5" ), IECore.InternedStringVectorData() ) self.assertEqual( instancer["out"].childNames( "/object/instances/cube/-10" ), IECore.InternedStringVectorData() ) + self.assertEqual( instancer["out"].childNames( "/object/instances/sphere/8000000001" ), IECore.InternedStringVectorData() ) + self.assertEqual( instancer["out"].childNames( "/object/instances/cube/8000000000" ), IECore.InternedStringVectorData() ) self.assertEqual( instancer["out"].object( "/object/instances" ), IECore.NullObject.defaultNullObject() ) self.assertEqual( instancer["out"].object( "/object/instances/sphere" ), IECore.NullObject.defaultNullObject() ) self.assertEqual( instancer["out"].object( "/object/instances/cube" ), IECore.NullObject.defaultNullObject() ) self.assertEqual( instancer["out"].object( "/object/instances/sphere/-5" ), sphere["out"].object( "/sphere" ) ) self.assertEqual( instancer["out"].object( "/object/instances/cube/-10" ), cube["out"].object( "/cube" ) ) + self.assertEqual( instancer["out"].object( "/object/instances/sphere/8000000001" ), sphere["out"].object( "/sphere" ) ) + self.assertEqual( instancer["out"].object( "/object/instances/cube/8000000000" ), cube["out"].object( "/cube" ) ) + + self.assertEqual( instancer["out"].attributes( "/object/instances/sphere/-5" )["intAttr"].value, -5 ) + self.assertEqual( instancer["out"].attributes( "/object/instances/cube/-10" )["intAttr"].value, -10 ) + + # We want to fully support int64 typed ids, but for reasons of backwards compatiblity and OSL support, + # we're still using int32 for the seed context variable, so these ids get wrapped around even in raw seeds + # mode. + self.assertEqual( instancer["out"].attributes( "/object/instances/sphere/8000000001" )["intAttr"].value, -589934591 ) + self.assertEqual( instancer["out"].attributes( "/object/instances/cube/8000000000" )["intAttr"].value, -589934592 ) + + self.assertEqual( + instancer["variations"].getValue(), + IECore.CompoundData( { 'seed' : IECore.IntData( 4 ), '' : IECore.IntData( 4 ) } ) + ) self.assertSceneValid( instancer["out"] ) @@ -2430,6 +2469,29 @@ def quant( x, q ): self.assertEncapsulatedRendersSame( instancer ) + + # We get different results if we change the id the seeds are based on + points["idTest"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.IntVectorData( + [ i * 37 for i in range( 100 ) ] + ) + ) + pointsSource["object"].setValue( points ) + instancer["id"].setValue( "idTest" ) + self.assertEqual( uniqueCounts(), { "floatVar" : 5, "color4fVar" : 4, "seed" : 64, "" : 98 } ) + + # Works the same using int64 ids + points["idTest"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.Int64VectorData( + [ i * 37 for i in range( 100 ) ] + ) + ) + pointsSource["object"].setValue( points ) + self.assertEqual( uniqueCounts(), { "floatVar" : 5, "color4fVar" : 4, "seed" : 64, "" : 98 } ) + + instancer["id"].setToDefault() + + # Now turn on time offset as well and play with everything together instancer["seeds"].setValue( 10 ) instancer["timeOffset"]["enabled"].setValue( True ) diff --git a/src/GafferScene/Instancer.cpp b/src/GafferScene/Instancer.cpp index 97ed413e554..035001b10ce 100644 --- a/src/GafferScene/Instancer.cpp +++ b/src/GafferScene/Instancer.cpp @@ -264,16 +264,71 @@ struct UniqueHashPrototypeContextVariable } }; +InternedString g_prototypeRootName( "root" ); +ConstInternedStringVectorDataPtr g_emptyNames = new InternedStringVectorData(); + +struct IdData +{ + IdData() : + intElements( nullptr ), int64Elements( nullptr ) + { + } + + void initialize( const Primitive *primitive, const std::string &name ) + { + if( const IntVectorData *intData = primitive->variableData( name ) ) + { + intElements = &intData->readable(); + } + else if( const Int64VectorData *int64Data = primitive->variableData( name ) ) + { + int64Elements = &int64Data->readable(); + } + + } + + size_t size() const + { + if( intElements ) + { + return intElements->size(); + } + else if( int64Elements ) + { + return int64Elements->size(); + } + else + { + return 0; + } + } + + int64_t element( size_t i ) const + { + if( intElements ) + { + return (*intElements)[i]; + } + else + { + return (*int64Elements)[i]; + } + } + + const std::vector *intElements; + const std::vector *int64Elements; + +}; + // We create a seed integer that corresponds to the id by hashing the id and then modulo'ing to // numSeeds, to create seeds in the range 0 .. numSeeds-1 that persistently correspond to the ids, // with a grouping pattern that can be changed with seedScramble -int seedForPoint( int index, const PrimitiveVariable *primVar, int numSeeds, int seedScramble ) +int seedForPoint( size_t index, const IdData &idData, int numSeeds, int seedScramble ) { - int id = index; - if( primVar ) + int64_t id = index; + if( idData.size() ) { - // TODO - the exception this will throw on non-int primvars may not be very clear to users - id = PrimitiveVariable::IndexedView( *primVar )[index]; + id = idData.element( index ); } // numSeeds is set to 0 when we're just passing through the id @@ -293,16 +348,25 @@ int seedForPoint( int index, const PrimitiveVariable *primVar, int numSeeds, int IECore::MurmurHash seedHash; seedHash.append( seedScramble ); - seedHash.append( id ); + + if( id <= INT32_MAX && id >= INT_MIN ) + { + // This branch shouldn't be needed, we'd like to just treat ids as 64 bit now ... + // but if we just took the branch below, that would changing the seeding of existing + // scenes. + seedHash.append( (int)id ); + } + else + { + seedHash.append( id ); + } + id = int( ( double( seedHash.h1() ) / double( UINT64_MAX ) ) * double( numSeeds ) ); id = id % numSeeds; // For the rare case h1 / max == 1.0, make sure we stay in range } return id; } -InternedString g_prototypeRootName( "root" ); -ConstInternedStringVectorDataPtr g_emptyNames = new InternedStringVectorData(); - } ////////////////////////////////////////////////////////////////////////// @@ -337,7 +401,6 @@ class Instancer::EngineData : public Data m_numPrototypes( 0 ), m_numValidPrototypes( 0 ), m_prototypeIndices( nullptr ), - m_ids( nullptr ), m_positions( nullptr ), m_orientations( nullptr ), m_scales( nullptr ), @@ -352,13 +415,10 @@ class Instancer::EngineData : public Data initPrototypes( mode, prototypeIndexName, rootsVariable, rootsList, prototypes ); - if( const IntVectorData *ids = m_primitive->variableData( idName ) ) + m_ids.initialize( m_primitive.get(), idName ); + if( m_ids.size() && m_ids.size() != numPoints() ) { - m_ids = &ids->readable(); - if( m_ids->size() != numPoints() ) - { - throw IECore::Exception( fmt::format( "Id primitive variable \"{}\" has incorrect size", idName ) ); - } + throw IECore::Exception( fmt::format( "Id primitive variable \"{}\" has incorrect size", idName ) ); } if( const V3fVectorData *p = m_primitive->variableData( position ) ) @@ -396,11 +456,11 @@ class Instancer::EngineData : public Data } } - if( m_ids ) + if( m_ids.size() ) { for( size_t i = 0, e = numPoints(); i < e; ++i ) { - int id = (*m_ids)[i]; + int64_t id = m_ids.element(i); auto ins = m_idsToPointIndices.try_emplace( id, i ); if( !ins.second ) { @@ -441,16 +501,16 @@ class Instancer::EngineData : public Data return m_primitive ? m_primitive->variableSize( PrimitiveVariable::Vertex ) : 0; } - size_t instanceId( size_t pointIndex ) const + int64_t instanceId( size_t pointIndex ) const { - return m_ids ? (*m_ids)[pointIndex] : pointIndex; + return m_ids.size() ? m_ids.element( pointIndex ) : pointIndex; } - size_t pointIndex( size_t i ) const + size_t pointIndex( int64_t i ) const { - if( !m_ids ) + if( !m_ids.size() ) { - if( i >= numPoints() ) + if( i >= (int64_t)numPoints() || i < 0 ) { throw IECore::Exception( fmt::format( "Instance id \"{}\" is invalid, instancer produces only {} children. Topology may have changed during shutter.", i, numPoints() ) ); } @@ -488,7 +548,7 @@ class Instancer::EngineData : public Data // If there are duplicates in the id list, then some point indices will be omitted - we // need to check each point index to see if it got assigned an id correctly - int id = (*m_ids)[pointIndex]; + int64_t id = m_ids.element( pointIndex ); if( m_idsToPointIndices.at(id) != pointIndex ) { @@ -632,7 +692,7 @@ class Instancer::EngineData : public Data if( v.seedMode ) { - scope.setAllocated( v.name, seedForPoint( pointIndex, v.primVar, v.numSeeds, v.seedScramble ) ); + scope.setAllocated( v.name, seedForPoint( pointIndex, m_ids, v.numSeeds, v.seedScramble ) ); continue; } @@ -660,7 +720,7 @@ class Instancer::EngineData : public Data { if( v.seedMode ) { - result.append( seedForPoint( pointIndex, v.primVar, v.numSeeds, v.seedScramble ) ); + result.append( seedForPoint( pointIndex, m_ids, v.numSeeds, v.seedScramble ) ); return; } @@ -915,13 +975,13 @@ class Instancer::EngineData : public Data std::vector m_prototypeIndexRemap; std::vector m_prototypeIndicesAlloc; const std::vector *m_prototypeIndices; - const std::vector *m_ids; + IdData m_ids; const std::vector *m_positions; const std::vector *m_orientations; const std::vector *m_scales; const std::vector *m_uniformScales; - using IdsToPointIndices = std::unordered_map ; + using IdsToPointIndices = std::unordered_map ; IdsToPointIndices m_idsToPointIndices; boost::container::flat_map m_attributeCreators; @@ -1695,7 +1755,7 @@ void Instancer::hash( const Gaffer::ValuePlug *output, const Gaffer::Context *co for( size_t i = r.begin(); i != r.end(); ++i ) { const size_t pointIndex = pointIndicesForPrototype[i]; - size_t instanceId = engine->instanceId( pointIndex ); + int64_t instanceId = engine->instanceId( pointIndex ); engine->setPrototypeContextVariables( pointIndex, scope ); IECore::MurmurHash instanceH; instanceH.append( instanceId ); @@ -1791,15 +1851,12 @@ void Instancer::compute( Gaffer::ValuePlug *output, const Gaffer::Context *conte if( seedContextName != "" ) { - const PrimitiveVariable *idPrimVar = findVertexVariable( primitive.get(), idPlug()->getValue() ); - if( idPrimVar && idPrimVar->data->typeId() != IntVectorDataTypeId ) - { - idPrimVar = nullptr; - } - int seeds = rawSeedPlug()->getValue() ? 0 : seedsPlug()->getValue(); int seedScramble = seedPermutationPlug()->getValue(); - prototypeContextVariables.push_back( { seedContextName, idPrimVar, 0, false, true, seeds, seedScramble } ); + + // We set seedMode to true here, which means rather than reading a given primvar, this context + // variable will be driven by whatever is driving id. + prototypeContextVariables.push_back( { seedContextName, nullptr, 0, false, true, seeds, seedScramble } ); } if( timeOffsetEnabled ) @@ -1948,7 +2005,7 @@ void Instancer::compute( Gaffer::ValuePlug *output, const Gaffer::Context *conte for( size_t i = r.begin(); i != r.end(); ++i ) { const size_t pointIndex = pointIndicesForPrototype[i]; - size_t instanceId = engine->instanceId( pointIndex ); + int64_t instanceId = engine->instanceId( pointIndex ); engine->setPrototypeContextVariables( pointIndex, scope ); ConstPathMatcherDataPtr instanceSet = prototypesPlug()->setPlug()->getValue(); PathMatcher pointInstanceSet = instanceSet->readable().subTree( *prototypeRoot ); @@ -2411,7 +2468,7 @@ IECore::ConstInternedStringVectorDataPtr Instancer::computeBranchChildNames( con // the ids, not the point indices, and must be sorted. So we need to allocate a // temp buffer of integer ids, before converting to strings. - std::vector ids; + std::vector ids; ids.reserve( pointIndicesForPrototype.size() ); const EngineData *engineData = esp->engine(); @@ -2427,7 +2484,7 @@ IECore::ConstInternedStringVectorDataPtr Instancer::computeBranchChildNames( con InternedStringVectorDataPtr childNamesData = new InternedStringVectorData; std::vector &childNames = childNamesData->writable(); childNames.reserve( ids.size() ); - for( size_t id : ids ) + for( int64_t id : ids ) { childNames.emplace_back( id ); } @@ -2987,7 +3044,7 @@ void Instancer::InstancerCapsule::render( IECoreScenePreview::Renderer *renderer attribs = proto->m_rendererAttributes.get(); } - int instanceId = engines[0]->instanceId( pointIndex ); + int64_t instanceId = engines[0]->instanceId( pointIndex ); if( !namePrefixLengths[protoIndex] ) @@ -3008,7 +3065,7 @@ void Instancer::InstancerCapsule::render( IECoreScenePreview::Renderer *renderer // up being named when they use the non-encapsulated hierarchy. std::string &name = names[ protoIndex ]; const int prefixLen = namePrefixLengths[ protoIndex ]; - name.resize( namePrefixLengths[protoIndex] + std::numeric_limits< int >::digits10 + 1 ); + name.resize( namePrefixLengths[protoIndex] + std::numeric_limits< int64_t >::digits10 + 1 ); name.resize( std::to_chars( &name[prefixLen], &(*name.end()), instanceId ).ptr - &name[0] ); IECoreScenePreview::Renderer::ObjectInterfacePtr objectInterface; From 85b334a181aef16e288f6f7b6c14ccaf54dba586 Mon Sep 17 00:00:00 2001 From: Daniel Dresser Date: Tue, 15 Oct 2024 19:42:23 -0700 Subject: [PATCH 11/34] Instancer : Document that "parent" plug no longer fully works --- python/GafferSceneUI/InstancerUI.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/python/GafferSceneUI/InstancerUI.py b/python/GafferSceneUI/InstancerUI.py index a6e7287b352..950f3e0a73d 100644 --- a/python/GafferSceneUI/InstancerUI.py +++ b/python/GafferSceneUI/InstancerUI.py @@ -345,12 +345,9 @@ def __init__( self, headings, toolTipOverride = "" ) : "description", """ - The object on which to make the instances. The - position, orientation and scale of the instances - are taken from per-vertex primitive variables on - this object. This is ignored when a filter is - connected, in which case the filter specifies - multiple objects to make the instances from. + Using the `parent` plug to select the source is now deprecated, please use a filter instead. + This plug is still supported for backwards compatibility, but is incompatible with recent features, + like accurately reporting variation counts. """, "layout:section", "Settings.General", From 5d53b2d890bb7df72eaed74b02af1122c7f3a35f Mon Sep 17 00:00:00 2001 From: ivanimanishi Date: Thu, 31 Oct 2024 09:42:07 -0700 Subject: [PATCH 12/34] renderCompatibility : Add default node name Provides compatibility with previously supported notation to create render nodes, like `GafferArnold.ArnoldRender()`. --- Changes.md | 5 +++++ startup/GafferArnold/renderCompatibility.py | 4 ++-- startup/GafferCycles/renderCompatibility.py | 4 ++-- startup/GafferDelight/renderCompatibility.py | 4 ++-- startup/GafferScene/renderCompatibility.py | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Changes.md b/Changes.md index f2d97dd7d4a..cf47c8b5ae6 100644 --- a/Changes.md +++ b/Changes.md @@ -6,6 +6,11 @@ Improvements - Instancer : Added support for 64 bit ints for ids ( matching what is loaded from USD ). +Fixes +----- + +- Render, InteractiveRender : Added default node name arguments to the compatibility shims for removed subclasses such as ArnoldRender. + 1.5.0.1 (relative to 1.5.0.0) ======= diff --git a/startup/GafferArnold/renderCompatibility.py b/startup/GafferArnold/renderCompatibility.py index b1d86079eb5..c557fcc2fb7 100644 --- a/startup/GafferArnold/renderCompatibility.py +++ b/startup/GafferArnold/renderCompatibility.py @@ -37,7 +37,7 @@ import GafferScene import GafferArnold -def __arnoldRender( name ) : +def __arnoldRender( name = "ArnoldRender" ) : node = GafferScene.Render( name ) node["renderer"].setValue( "Arnold" ) @@ -45,7 +45,7 @@ def __arnoldRender( name ) : GafferArnold.ArnoldRender = __arnoldRender -def __interactiveArnoldRender( name ) : +def __interactiveArnoldRender( name = "InteractiveArnoldRender" ) : node = GafferScene.InteractiveRender( name ) node["renderer"].setValue( "Arnold" ) diff --git a/startup/GafferCycles/renderCompatibility.py b/startup/GafferCycles/renderCompatibility.py index bf459c53746..91b94ba1e55 100644 --- a/startup/GafferCycles/renderCompatibility.py +++ b/startup/GafferCycles/renderCompatibility.py @@ -37,7 +37,7 @@ import GafferScene import GafferCycles -def __cyclesRender( name ) : +def __cyclesRender( name = "CyclesRender" ) : node = GafferScene.Render( name ) node["renderer"].setValue( "Cycles" ) @@ -45,7 +45,7 @@ def __cyclesRender( name ) : GafferCycles.CyclesRender = __cyclesRender -def __interactiveCyclesRender( name ) : +def __interactiveCyclesRender( name = "InteractiveCyclesRender" ) : node = GafferScene.InteractiveRender( name ) node["renderer"].setValue( "Cycles" ) diff --git a/startup/GafferDelight/renderCompatibility.py b/startup/GafferDelight/renderCompatibility.py index f624a3408e2..f389c1e80be 100644 --- a/startup/GafferDelight/renderCompatibility.py +++ b/startup/GafferDelight/renderCompatibility.py @@ -37,7 +37,7 @@ import GafferScene import GafferDelight -def __delightRender( name ) : +def __delightRender( name = "DelightRender" ) : node = GafferScene.Render( name ) node["renderer"].setValue( "3Delight" ) @@ -45,7 +45,7 @@ def __delightRender( name ) : GafferDelight.DelightRender = __delightRender -def __interactiveDelightRender( name ) : +def __interactiveDelightRender( name = "InteractiveDelightRender" ) : node = GafferScene.InteractiveRender( name ) node["renderer"].setValue( "3Delight" ) diff --git a/startup/GafferScene/renderCompatibility.py b/startup/GafferScene/renderCompatibility.py index 37f78ada664..62498785926 100644 --- a/startup/GafferScene/renderCompatibility.py +++ b/startup/GafferScene/renderCompatibility.py @@ -36,7 +36,7 @@ import GafferScene -def __openGLRender( name ) : +def __openGLRender( name = "OpenGLRender" ) : node = GafferScene.Render( name ) node["renderer"].setValue( "OpenGL" ) From 1e9036fb482f94bcced77ad6ef76527f1c33810e Mon Sep 17 00:00:00 2001 From: Daniel Dresser Date: Thu, 31 Oct 2024 15:34:35 -0700 Subject: [PATCH 13/34] Instancer : Basic test that render hashes include proto root --- python/GafferSceneTest/InstancerTest.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/python/GafferSceneTest/InstancerTest.py b/python/GafferSceneTest/InstancerTest.py index 6e70822c760..9bf22d09ed6 100644 --- a/python/GafferSceneTest/InstancerTest.py +++ b/python/GafferSceneTest/InstancerTest.py @@ -3022,6 +3022,29 @@ def testVaryingPrimvars( self ) : self.assertEqual( [ instancer["out"].attributes( "/object/instances/%s/%i/%s" % ("abdc"[i],i,"abdc"[i]) )["testAttribute"].value for i in range(4) ], [ 42.0 ] * 4 ) self.assertEqual( [ instancer["out"].attributes( "/object/instances/%s/%i" % ("abdc"[i],i) ).get("user:varyingFloat") for i in range(4) ], [ None ] * 4 ) + def testRenderHashes( self ) : + + script = self.buildPrototypeRootsScript() + script["instancer"]["prototypeMode"].setValue( GafferScene.Instancer.PrototypeMode.IndexedRootsList ) + script["instancer"]["prototypeRootsList"].setValue( IECore.StringVectorData( [ + "/foo", "/bar" + ] ) ) + script["instancer"]["encapsulate"].setValue( True ) + + renderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer( GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch ) + + script["instancer"]["out"].object( "/object/instances" ).render( renderer ) + + # Make sure that the capsules we produce have different hashes when the prototype roots are different + rootsByHash = {} + for n in renderer.capturedObjectNames(): + co = renderer.capturedObject( n ).capturedSamples()[0] + if co.hash() in rootsByHash: + with self.subTest( location = n ) : + self.assertEqual( co.root(), rootsByHash[ co.hash() ] ) + else: + rootsByHash[ co.hash() ] = co.root() + @GafferTest.TestRunner.PerformanceTestMethod( repeat = 10 ) def testBoundPerformance( self ) : From 5843546e65e6efb35dfb81c66746ef765317ae54 Mon Sep 17 00:00:00 2001 From: ivanimanishi Date: Thu, 31 Oct 2024 10:05:44 -0700 Subject: [PATCH 14/34] GafferUITest : Fix `assertNodeUIsHaveExpectedLifetime()` test This test was raising an exception for nodes that are supposed to be "invisible", and therefore had no `NodeGadget`. --- Changes.md | 1 + python/GafferUITest/TestCase.py | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Changes.md b/Changes.md index cf47c8b5ae6..ff136283d3d 100644 --- a/Changes.md +++ b/Changes.md @@ -10,6 +10,7 @@ Fixes ----- - Render, InteractiveRender : Added default node name arguments to the compatibility shims for removed subclasses such as ArnoldRender. +- GafferUITest : Fixed `assertNodeUIsHaveExpectedLifetime()` test for invisible nodes. 1.5.0.1 (relative to 1.5.0.0) ======= diff --git a/python/GafferUITest/TestCase.py b/python/GafferUITest/TestCase.py index 46e02a51c34..523ebb02301 100644 --- a/python/GafferUITest/TestCase.py +++ b/python/GafferUITest/TestCase.py @@ -169,11 +169,13 @@ def assertNodeUIsHaveExpectedLifetime( self, module ) : weakScript = weakref.ref( script ) nodeGadget = GafferUI.NodeGadget.create( script["node"] ) - weakNodeGadget = weakref.ref( nodeGadget ) + if nodeGadget : + weakNodeGadget = weakref.ref( nodeGadget ) + del nodeGadget + self.assertIsNone( weakNodeGadget() ) - del window, nodeUI, nodeGadget + del window, nodeUI self.assertIsNone( weakNodeUI() ) - self.assertIsNone( weakNodeGadget() ) del script self.assertIsNone( weakScript() ) From 3132819cd85d3ab0cce7b0114d25d94bcacd1962 Mon Sep 17 00:00:00 2001 From: Daniel Dresser Date: Wed, 9 Oct 2024 17:55:10 -0700 Subject: [PATCH 15/34] Instancer : Add "inactiveIds" plug --- Changes.md | 4 +- include/GafferScene/Instancer.h | 3 + python/GafferSceneTest/InstancerTest.py | 78 +++++++++ python/GafferSceneUI/InstancerUI.py | 20 +++ src/GafferScene/Instancer.cpp | 216 +++++++++++++++++++----- 5 files changed, 274 insertions(+), 47 deletions(-) diff --git a/Changes.md b/Changes.md index ff136283d3d..7aa840169ad 100644 --- a/Changes.md +++ b/Changes.md @@ -4,7 +4,9 @@ Improvements ------------ -- Instancer : Added support for 64 bit ints for ids ( matching what is loaded from USD ). +- Instancer : + - Added `inactiveIds` plug for selecting primitive variables to disable some instances. + - Added support for 64 bit integer ids (matching what is loaded from USD). Fixes ----- diff --git a/include/GafferScene/Instancer.h b/include/GafferScene/Instancer.h index 3a01bead8bf..f6043538c07 100644 --- a/include/GafferScene/Instancer.h +++ b/include/GafferScene/Instancer.h @@ -138,6 +138,9 @@ class GAFFERSCENE_API Instancer : public BranchCreator Gaffer::StringPlug *scalePlug(); const Gaffer::StringPlug *scalePlug() const; + Gaffer::StringPlug *inactiveIdsPlug(); + const Gaffer::StringPlug *inactiveIdsPlug() const; + Gaffer::StringPlug *attributesPlug(); const Gaffer::StringPlug *attributesPlug() const; diff --git a/python/GafferSceneTest/InstancerTest.py b/python/GafferSceneTest/InstancerTest.py index 9bf22d09ed6..1bbfd882c0c 100644 --- a/python/GafferSceneTest/InstancerTest.py +++ b/python/GafferSceneTest/InstancerTest.py @@ -725,6 +725,84 @@ def testTransform( self ) : ) self.assertEncapsulatedRendersSame( instancer ) + def testInactiveIds( self ) : + + points = IECoreScene.PointsPrimitive( IECore.V3fVectorData( [ imath.V3f( x, 0, 0 ) for x in range( 0, 10 ) ] ) ) + points["inactiveIdsTest"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Constant, + IECore.IntVectorData( [ 3, 5, 7 ] ) + ) + points["inactiveIdsTest64"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Constant, + IECore.Int64VectorData( [ 4, 6 ] ) + ) + points["inactive"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECore.BoolVectorData( [ 0, 0, 1, 0, 0, 1, 1, 0, 1, 1 ] ) + ) + points["inactiveInt"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECore.IntVectorData( [ 0, 0, 1, 0, 0, 1, 1, 0, 1, 1 ] ) + ) + points["badInactive"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Constant, + IECore.IntVectorData( [ 13 ] ) + ) + points["alternateIds"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECore.IntVectorData( [ i + 7 for i in range( 10 ) ] ) + ) + + objectToScene = GafferScene.ObjectToScene() + objectToScene["object"].setValue( points ) + + sphere = GafferScene.Sphere() + + pointsFilter = GafferScene.PathFilter() + pointsFilter["paths"].setValue( IECore.StringVectorData( [ "/object" ] ) ) + + instancer = GafferScene.Instancer() + instancer["in"].setInput( objectToScene["out"] ) + instancer["prototypes"].setInput( sphere["out"] ) + instancer["filter"].setInput( pointsFilter["out"] ) + + self.assertEqual( instancer["out"].childNames( "/object/instances/sphere" ), IECore.InternedStringVectorData( [ str( i ) for i in range( 10 ) ] ) ) + + instancer["inactiveIds"].setValue( "inactiveIdsTest" ) + self.assertEqual( instancer["out"].childNames( "/object/instances/sphere" ), IECore.InternedStringVectorData( [ "0", "1", "2", "4", "6", "8", "9" ] ) ) + + instancer["inactiveIds"].setValue( "inactiveIdsTest64" ) + self.assertEqual( instancer["out"].childNames( "/object/instances/sphere" ), IECore.InternedStringVectorData( [ "0", "1", "2", "3", "5", "7", "8", "9" ] ) ) + + instancer["inactiveIds"].setValue( "inactive" ) + self.assertEqual( instancer["out"].childNames( "/object/instances/sphere" ), IECore.InternedStringVectorData( [ "0", "1", "3", "4", "7" ] ) ) + + instancer["inactiveIds"].setValue( "inactiveInt" ) + self.assertEqual( instancer["out"].childNames( "/object/instances/sphere" ), IECore.InternedStringVectorData( [ "0", "1", "3", "4", "7" ] ) ) + + instancer["inactiveIds"].setValue( "inactiveIdsTest inactiveIdsTest64" ) + self.assertEqual( instancer["out"].childNames( "/object/instances/sphere" ), IECore.InternedStringVectorData( [ "0", "1", "2", "8", "9" ] ) ) + + instancer["inactiveIds"].setValue( "inactiveIdsTest inactiveIdsTest64 inactive" ) + self.assertEqual( instancer["out"].childNames( "/object/instances/sphere" ), IECore.InternedStringVectorData( [ "0", "1" ] ) ) + + # If the id is out of bounds, nothing happens + instancer["inactiveIds"].setValue( "badInactive" ) + self.assertEqual( instancer["out"].childNames( "/object/instances/sphere" ), IECore.InternedStringVectorData( [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" ] ) ) + + instancer["id"].setValue( "alternateIds" ) + # A vertex variable applies based on vertex position in the list + instancer["inactiveIds"].setValue( "inactive" ) + self.assertEqual( instancer["out"].childNames( "/object/instances/sphere" ), IECore.InternedStringVectorData( [ "7", "8", "10", "11", "14" ] ) ) + + # An id list matches based on ids + instancer["inactiveIds"].setValue( "inactive inactiveIdsTest" ) + self.assertEqual( instancer["out"].childNames( "/object/instances/sphere" ), IECore.InternedStringVectorData( [ "8", "10", "11", "14" ] ) ) + + instancer["inactiveIds"].setValue( "badInactive" ) + self.assertEqual( instancer["out"].childNames( "/object/instances/sphere" ), IECore.InternedStringVectorData( [ "7", "8", "9", "10", "11", "12", "14", "15", "16" ] ) ) + + def testAnimation( self ) : pointA = IECoreScene.PointsPrimitive( IECore.V3fVectorData( [ diff --git a/python/GafferSceneUI/InstancerUI.py b/python/GafferSceneUI/InstancerUI.py index 950f3e0a73d..59c94586f42 100644 --- a/python/GafferSceneUI/InstancerUI.py +++ b/python/GafferSceneUI/InstancerUI.py @@ -284,6 +284,7 @@ def __init__( self, headings, toolTipOverride = "" ) : "layout:section:Settings.General:collapsed", False, "layout:section:Settings.Transforms:collapsed", False, + "layout:section:Settings.Inactive Ids:collapsed", False, "layout:section:Settings.Attributes:collapsed", False, "layout:activator:modeIsIndexedRootsList", lambda node : node["prototypeMode"].getValue() == GafferScene.Instancer.PrototypeMode.IndexedRootsList, @@ -546,6 +547,25 @@ def __init__( self, headings, toolTipOverride = "" ) : ], + "inactiveIds" : [ + + "description", + """ + A space separated list of names of primitive variables specifying instances to make inactive. + Inactive instances are not output from the instancer or rendered. + + Each primitive variable either must be a constant vector of type Int or Int64 with a list of + matching ids to deactivate, or it must be a vertex bool primitive variable, in which case it + will deactivate the instance for the corresponding vertex if the value is true. + """, + + # This user default will pick up any of the standard USD ways of controlling this. + "userDefault", "inactiveIds invisibleIds", + + "layout:section", "Settings.Inactive Ids", + + ], + "attributes" : [ "description", diff --git a/src/GafferScene/Instancer.cpp b/src/GafferScene/Instancer.cpp index 035001b10ce..15ab8535796 100644 --- a/src/GafferScene/Instancer.cpp +++ b/src/GafferScene/Instancer.cpp @@ -393,6 +393,7 @@ class Instancer::EngineData : public Data const std::string &position, const std::string &orientation, const std::string &scale, + const std::string &inactiveIds, const std::string &attributes, const std::string &attributePrefix, const std::vector< PrototypeContextVariable > &prototypeContextVariables @@ -405,8 +406,7 @@ class Instancer::EngineData : public Data m_orientations( nullptr ), m_scales( nullptr ), m_uniformScales( nullptr ), - m_prototypeContextVariables( prototypeContextVariables ), - m_hasDuplicates( false ) + m_prototypeContextVariables( prototypeContextVariables ) { if( !m_primitive ) { @@ -464,16 +464,128 @@ class Instancer::EngineData : public Data auto ins = m_idsToPointIndices.try_emplace( id, i ); if( !ins.second ) { + // We have multiple indices trying to use this id. if( !omitDuplicateIds ) { throw IECore::Exception( fmt::format( "Instance id \"{}\" is duplicated at index {} and {}. This probably indicates invalid source data, if you want to hack around it, you can set \"omitDuplicateIds\".", id, m_idsToPointIndices[id], i ) ); } - // Invalidate the existing entry in the m_idsToPointIndices map - since we can't assign - // a consistent point index to this id, we omit all point indices that try to use this - // id - ins.first->second = std::numeric_limits::max(); - m_hasDuplicates = true; + if( !m_indicesInactive.size() ) + { + m_indicesInactive.resize( numPoints(), false ); + } + + // If we're omitting duplicate ids, then we need to omit both the current index, and + // the index that first tried to use this id. + m_indicesInactive[ i ] = true; + m_indicesInactive[ ins.first->second ] = true; + } + } + } + + std::vector inactiveIdVarNames; + IECore::StringAlgo::tokenize( inactiveIds, ' ', inactiveIdVarNames ); + for( std::string &inactiveIdVarName : inactiveIdVarNames ) + { + if( m_primitive->variables.find( inactiveIdVarName ) == m_primitive->variables.end() ) + { + continue; + } + + const PrimitiveVariable *vertexInactiveVar = findVertexVariable( m_primitive.get(), inactiveIdVarName ); + if( vertexInactiveVar ) + { + if( IECore::size( vertexInactiveVar->data.get() ) != numPoints() ) + { + throw IECore::Exception( fmt::format( "Inactive primitive variable \"{}\" has incorrect size", inactiveIdVarName ) ); + } + + if( const auto *vertexInactiveData = IECore::runTimeCast( vertexInactiveVar->data.get() ) ) + { + const std::vector &vertexInactive = vertexInactiveData->readable(); + + if( !m_indicesInactive.size() ) + { + // If we don't already have an inactive array set up, we can just directly copy the data + // from a vertex primitive variable. Technically, we might not even need to do this copy, + // if there aren't any other inactive vars we're merging with, we could just have a + // separate way of storing a const pointer for this case, but given that this data is + // 32X smaller than any of our other per-vertex data anyway, it's probably fine to pay + // the cost of copying it in exchange for slightly simpler code. + m_indicesInactive = vertexInactive; + } + else + { + for( size_t i = 0; i < vertexInactive.size(); i++ ) + { + if( vertexInactive[i] ) + { + m_indicesInactive[ i ] = true; + } + } + } + } + else if( const auto *vertexInactiveIntData = IECore::runTimeCast( vertexInactiveVar->data.get() ) ) + { + const std::vector &vertexInactiveInt = vertexInactiveIntData->readable(); + + if( !m_indicesInactive.size() ) + { + m_indicesInactive.resize( numPoints(), false ); + } + + for( size_t i = 0; i < vertexInactiveInt.size(); i++ ) + { + if( vertexInactiveInt[i] ) + { + m_indicesInactive[ i ] = true; + } + } + } + + continue; + } + + IdData idData; + idData.initialize( m_primitive.get(), inactiveIdVarName ); + + size_t idSize = idData.size(); + if( !idSize ) + { + continue; + } + + if( !m_indicesInactive.size() ) + { + m_indicesInactive.resize( numPoints(), false ); + } + + if( m_idsToPointIndices.size() ) + { + for( size_t i = 0; i < idSize; i++ ) + { + auto it = m_idsToPointIndices.find( idData.element(i) ); + if( it == m_idsToPointIndices.end() ) + { + // I wish I could throw here ... it would be a really helpful clue to get an error + // if you've accidentally chosen a bad id. But ids might be changing over time, so + // we probably need to allow someone to deactivate an id even if it doesn't exist + // on all frames. + continue; + } + m_indicesInactive[ it->second ] = true; + } + } + else + { + for( size_t i = 0; i < idSize; i++ ) + { + int64_t id = idData.element(i); + if( id < 0 || id >= (int64_t)m_indicesInactive.size() ) + { + continue; + } + m_indicesInactive[ id ] = true; } } } @@ -543,14 +655,12 @@ class Instancer::EngineData : public Data return -1; } - if( m_hasDuplicates ) + if( m_indicesInactive.size() ) { - // If there are duplicates in the id list, then some point indices will be omitted - we - // need to check each point index to see if it got assigned an id correctly - - int64_t id = m_ids.element( pointIndex ); - - if( m_idsToPointIndices.at(id) != pointIndex ) + // If this point is tagged as inactive ( could be due to a user specified inactiveIds, + // or due to an id collision when omitDuplicateIds is set ), then we return -1 for + // the prototype, which means to omit this point. + if( m_indicesInactive[pointIndex] ) { return -1; } @@ -989,7 +1099,7 @@ class Instancer::EngineData : public Data const std::vector< PrototypeContextVariable > m_prototypeContextVariables; - bool m_hasDuplicates; + std::vector m_indicesInactive; friend Instancer::EngineSplitPrototypesData; }; @@ -1032,7 +1142,7 @@ class Instancer::EngineSplitPrototypesData : public Data pointIndicesForPrototypeIndex[ constantPrototypeIndex ].reserve( m_engineData->numPoints() ); } - if( constantPrototypeIndex != -1 && !m_engineData->m_hasDuplicates ) + if( constantPrototypeIndex != -1 && !m_engineData->m_indicesInactive.size() ) { // If there's a single prototype, and no indices are being omitted because they are duplicates, // then the list of point indices for the prototype is just an identity map of all integers @@ -1260,6 +1370,7 @@ Instancer::Instancer( const std::string &name ) addChild( new StringPlug( "position", Plug::In, "P" ) ); addChild( new StringPlug( "orientation", Plug::In ) ); addChild( new StringPlug( "scale", Plug::In ) ); + addChild( new StringPlug( "inactiveIds", Plug::In, "" ) ); addChild( new StringPlug( "attributes", Plug::In ) ); addChild( new StringPlug( "attributePrefix", Plug::In ) ); addChild( new BoolPlug( "encapsulate", Plug::In ) ); @@ -1401,154 +1512,164 @@ const Gaffer::StringPlug *Instancer::scalePlug() const return getChild( g_firstPlugIndex + 10 ); } -Gaffer::StringPlug *Instancer::attributesPlug() +Gaffer::StringPlug *Instancer::inactiveIdsPlug() { return getChild( g_firstPlugIndex + 11 ); } -const Gaffer::StringPlug *Instancer::attributesPlug() const +const Gaffer::StringPlug *Instancer::inactiveIdsPlug() const { return getChild( g_firstPlugIndex + 11 ); } -Gaffer::StringPlug *Instancer::attributePrefixPlug() +Gaffer::StringPlug *Instancer::attributesPlug() { return getChild( g_firstPlugIndex + 12 ); } -const Gaffer::StringPlug *Instancer::attributePrefixPlug() const +const Gaffer::StringPlug *Instancer::attributesPlug() const { return getChild( g_firstPlugIndex + 12 ); } +Gaffer::StringPlug *Instancer::attributePrefixPlug() +{ + return getChild( g_firstPlugIndex + 13 ); +} + +const Gaffer::StringPlug *Instancer::attributePrefixPlug() const +{ + return getChild( g_firstPlugIndex + 13 ); +} + Gaffer::BoolPlug *Instancer::encapsulatePlug() { - return getChild( g_firstPlugIndex + 13 ); + return getChild( g_firstPlugIndex + 14 ); } const Gaffer::BoolPlug *Instancer::encapsulatePlug() const { - return getChild( g_firstPlugIndex + 13 ); + return getChild( g_firstPlugIndex + 14 ); } Gaffer::BoolPlug *Instancer::seedEnabledPlug() { - return getChild( g_firstPlugIndex + 14 ); + return getChild( g_firstPlugIndex + 15 ); } const Gaffer::BoolPlug *Instancer::seedEnabledPlug() const { - return getChild( g_firstPlugIndex + 14 ); + return getChild( g_firstPlugIndex + 15 ); } Gaffer::StringPlug *Instancer::seedVariablePlug() { - return getChild( g_firstPlugIndex + 15 ); + return getChild( g_firstPlugIndex + 16 ); } const Gaffer::StringPlug *Instancer::seedVariablePlug() const { - return getChild( g_firstPlugIndex + 15 ); + return getChild( g_firstPlugIndex + 16 ); } Gaffer::IntPlug *Instancer::seedsPlug() { - return getChild( g_firstPlugIndex + 16 ); + return getChild( g_firstPlugIndex + 17 ); } const Gaffer::IntPlug *Instancer::seedsPlug() const { - return getChild( g_firstPlugIndex + 16 ); + return getChild( g_firstPlugIndex + 17 ); } Gaffer::IntPlug *Instancer::seedPermutationPlug() { - return getChild( g_firstPlugIndex + 17 ); + return getChild( g_firstPlugIndex + 18 ); } const Gaffer::IntPlug *Instancer::seedPermutationPlug() const { - return getChild( g_firstPlugIndex + 17 ); + return getChild( g_firstPlugIndex + 18 ); } Gaffer::BoolPlug *Instancer::rawSeedPlug() { - return getChild( g_firstPlugIndex + 18 ); + return getChild( g_firstPlugIndex + 19 ); } const Gaffer::BoolPlug *Instancer::rawSeedPlug() const { - return getChild( g_firstPlugIndex + 18 ); + return getChild( g_firstPlugIndex + 19 ); } Gaffer::ValuePlug *Instancer::contextVariablesPlug() { - return getChild( g_firstPlugIndex + 19 ); + return getChild( g_firstPlugIndex + 20 ); } const Gaffer::ValuePlug *Instancer::contextVariablesPlug() const { - return getChild( g_firstPlugIndex + 19 ); + return getChild( g_firstPlugIndex + 20 ); } GafferScene::Instancer::ContextVariablePlug *Instancer::timeOffsetPlug() { - return getChild( g_firstPlugIndex + 20 ); + return getChild( g_firstPlugIndex + 21 ); } const GafferScene::Instancer::ContextVariablePlug *Instancer::timeOffsetPlug() const { - return getChild( g_firstPlugIndex + 20 ); + return getChild( g_firstPlugIndex + 21 ); } Gaffer::AtomicCompoundDataPlug *Instancer::variationsPlug() { - return getChild( g_firstPlugIndex + 21 ); + return getChild( g_firstPlugIndex + 22 ); } const Gaffer::AtomicCompoundDataPlug *Instancer::variationsPlug() const { - return getChild( g_firstPlugIndex + 21 ); + return getChild( g_firstPlugIndex + 22 ); } Gaffer::ObjectPlug *Instancer::enginePlug() { - return getChild( g_firstPlugIndex + 22 ); + return getChild( g_firstPlugIndex + 23 ); } const Gaffer::ObjectPlug *Instancer::enginePlug() const { - return getChild( g_firstPlugIndex + 22 ); + return getChild( g_firstPlugIndex + 23 ); } Gaffer::ObjectPlug *Instancer::engineSplitPrototypesPlug() { - return getChild( g_firstPlugIndex + 23 ); + return getChild( g_firstPlugIndex + 24 ); } const Gaffer::ObjectPlug *Instancer::engineSplitPrototypesPlug() const { - return getChild( g_firstPlugIndex + 23 ); + return getChild( g_firstPlugIndex + 24 ); } GafferScene::ScenePlug *Instancer::capsuleScenePlug() { - return getChild( g_firstPlugIndex + 24 ); + return getChild( g_firstPlugIndex + 25 ); } const GafferScene::ScenePlug *Instancer::capsuleScenePlug() const { - return getChild( g_firstPlugIndex + 24 ); + return getChild( g_firstPlugIndex + 25 ); } Gaffer::PathMatcherDataPlug *Instancer::setCollaboratePlug() { - return getChild( g_firstPlugIndex + 25 ); + return getChild( g_firstPlugIndex + 26 ); } const Gaffer::PathMatcherDataPlug *Instancer::setCollaboratePlug() const { - return getChild( g_firstPlugIndex + 25 ); + return getChild( g_firstPlugIndex + 26 ); } void Instancer::affects( const Plug *input, AffectedPlugsContainer &outputs ) const @@ -1568,6 +1689,7 @@ void Instancer::affects( const Plug *input, AffectedPlugsContainer &outputs ) co input == positionPlug() || input == orientationPlug() || input == scalePlug() || + input == inactiveIdsPlug() || input == attributesPlug() || input == attributePrefixPlug() || input == seedEnabledPlug() || @@ -1663,6 +1785,7 @@ void Instancer::hash( const Gaffer::ValuePlug *output, const Gaffer::Context *co positionPlug()->hash( h ); orientationPlug()->hash( h ); scalePlug()->hash( h ); + inactiveIdsPlug()->hash( h ); attributesPlug()->hash( h ); attributePrefixPlug()->hash( h ); encapsulatePlug()->hash( h ); @@ -1890,6 +2013,7 @@ void Instancer::compute( Gaffer::ValuePlug *output, const Gaffer::Context *conte positionPlug()->getValue(), orientationPlug()->getValue(), scalePlug()->getValue(), + inactiveIdsPlug()->getValue(), attributesPlug()->getValue(), attributePrefixPlug()->getValue(), prototypeContextVariables From c1cff2f9e6bb58fa041b509f628042d0b36573d6 Mon Sep 17 00:00:00 2001 From: Daniel Dresser Date: Thu, 31 Oct 2024 17:05:06 -0700 Subject: [PATCH 16/34] Instancer : Hypothetical support for point clouds with >2^32 points --- src/GafferScene/Instancer.cpp | 39 +++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/GafferScene/Instancer.cpp b/src/GafferScene/Instancer.cpp index 15ab8535796..0761984c9fe 100644 --- a/src/GafferScene/Instancer.cpp +++ b/src/GafferScene/Instancer.cpp @@ -199,14 +199,14 @@ struct PrototypeContextVariable struct AccessPrototypeContextVariable { template< class T> - void operator()( const TypedData> *data, const PrototypeContextVariable &v, int index, Context::EditableScope &scope ) + void operator()( const TypedData> *data, const PrototypeContextVariable &v, size_t index, Context::EditableScope &scope ) { T raw = PrimitiveVariable::IndexedView( *v.primVar )[index]; T value = quantize( raw, v.quantize ); scope.setAllocated( v.name, value ); } - void operator()( const TypedData> *data, const PrototypeContextVariable &v, int index, Context::EditableScope &scope ) + void operator()( const TypedData> *data, const PrototypeContextVariable &v, size_t index, Context::EditableScope &scope ) { float raw = PrimitiveVariable::IndexedView( *v.primVar )[index]; float value = quantize( raw, v.quantize ); @@ -221,7 +221,7 @@ struct AccessPrototypeContextVariable } } - void operator()( const TypedData> *data, const PrototypeContextVariable &v, int index, Context::EditableScope &scope ) + void operator()( const TypedData> *data, const PrototypeContextVariable &v, size_t index, Context::EditableScope &scope ) { int raw = PrimitiveVariable::IndexedView( *v.primVar )[index]; int value = quantize( raw, v.quantize ); @@ -236,7 +236,7 @@ struct AccessPrototypeContextVariable } } - void operator()( const Data *data, const PrototypeContextVariable &v, int index, Context::EditableScope &scope ) + void operator()( const Data *data, const PrototypeContextVariable &v, size_t index, Context::EditableScope &scope ) { throw IECore::Exception( "Context variable prim vars must contain vector data" ); } @@ -250,7 +250,7 @@ struct AccessPrototypeContextVariable struct UniqueHashPrototypeContextVariable { template< class T> - void operator()( const TypedData> *data, const PrototypeContextVariable &v, int index, MurmurHash &contextHash ) + void operator()( const TypedData> *data, const PrototypeContextVariable &v, size_t index, MurmurHash &contextHash ) { T raw = PrimitiveVariable::IndexedView( *v.primVar )[index]; @@ -752,7 +752,7 @@ class Instancer::EngineData : public Data boost::unordered_set< IECore::MurmurHash > totalHashAccumulate; size_t n = numPoints(); - for( unsigned int i = 0; i < n; i++ ) + for( size_t i = 0; i < n; i++ ) { int protoIndex = prototypeIndex( i ); if( protoIndex == -1 ) @@ -794,7 +794,7 @@ class Instancer::EngineData : public Data // Set the context variables in the context for this point index, based on the m_prototypeContextVariables // set up for this EngineData - void setPrototypeContextVariables( int pointIndex, Context::EditableScope &scope ) const + void setPrototypeContextVariables( size_t pointIndex, Context::EditableScope &scope ) const { for( unsigned int i = 0; i < m_prototypeContextVariables.size(); i++ ) { @@ -826,7 +826,7 @@ class Instancer::EngineData : public Data // Needs to match setPrototypeContextVariables above, except that it operates on one // PrototypeContextVariable at a time instead of iterating through them - void hashPrototypeContextVariable( int pointIndex, const PrototypeContextVariable &v, IECore::MurmurHash &result ) const + void hashPrototypeContextVariable( size_t pointIndex, const PrototypeContextVariable &v, IECore::MurmurHash &result ) const { if( v.seedMode ) { @@ -1135,7 +1135,7 @@ class Instancer::EngineSplitPrototypesData : public Data } // We need a list of which point indices belong to each prototype - std::vector< std::vector > pointIndicesForPrototypeIndex( m_engineData->m_numPrototypes ); + std::vector< std::vector > pointIndicesForPrototypeIndex( m_engineData->m_numPrototypes ); // Pre allocate if there's just one prototype, since we know the length will just be every point if( constantPrototypeIndex != -1 ) { @@ -1192,7 +1192,7 @@ class Instancer::EngineSplitPrototypesData : public Data return m_engineData.get(); } - const std::vector & pointIndicesForPrototype( const IECore::InternedString &prototypeName ) const + const std::vector & pointIndicesForPrototype( const IECore::InternedString &prototypeName ) const { return m_pointIndicesForPrototype.at( prototypeName ); } @@ -1201,7 +1201,7 @@ class Instancer::EngineSplitPrototypesData : public Data protected : ConstEngineDataPtr m_engineData; - std::unordered_map< InternedString, std::vector > m_pointIndicesForPrototype; + std::unordered_map< InternedString, std::vector > m_pointIndicesForPrototype; }; @@ -1865,7 +1865,7 @@ void Instancer::hash( const Gaffer::ValuePlug *output, const Gaffer::Context *co for( const auto &prototypeName : engine->prototypeNames()->readable() ) { - const std::vector &pointIndicesForPrototype = esp->pointIndicesForPrototype( prototypeName ); + const std::vector &pointIndicesForPrototype = esp->pointIndicesForPrototype( prototypeName ); std::atomic h1Accum( 0 ), h2Accum( 0 ); const ThreadState &threadState = ThreadState::current(); @@ -2051,7 +2051,6 @@ void Instancer::compute( Gaffer::ValuePlug *output, const Gaffer::Context *conte CompoundDataPtr result = new CompoundData; - std::vector< int > numUnique; std::vector< InternedString > outputNames; if( perLocationHashes.size() == 0 ) { @@ -2061,6 +2060,10 @@ void Instancer::compute( Gaffer::ValuePlug *output, const Gaffer::Context *conte } else { + // \todo - we should technically be returning Int64Data in this compound, in case someone + // uses rawSeed mode with more than 2^32 points. But this would be a compatibility break, + // so I'm not changing it now. + if( perLocationHashes.size() == 1 ) { // We only have one location, so we can just output the sizes of the hash sets @@ -2114,7 +2117,7 @@ void Instancer::compute( Gaffer::ValuePlug *output, const Gaffer::Context *conte branchPath.back() = prototypeName; const ScenePlug::ScenePath *prototypeRoot = engine->prototypeRoot( prototypeName ); - const std::vector &pointIndicesForPrototype = esp->pointIndicesForPrototype( prototypeName ); + const std::vector &pointIndicesForPrototype = esp->pointIndicesForPrototype( prototypeName ); tbb::spin_mutex instanceMutex; branchPath.emplace_back( InternedString() ); @@ -2255,7 +2258,7 @@ Imath::Box3f Instancer::computeBranchBound( const ScenePath &sourcePath, const S childBound = prototypesPlug()->boundPlug()->getValue(); } - const std::vector &pointIndicesForPrototype = esp->pointIndicesForPrototype( branchPath.back() ); + const std::vector &pointIndicesForPrototype = esp->pointIndicesForPrototype( branchPath.back() ); // TODO - might be worth using a looser approximation - expand point cloud bound by largest diagonal of // prototype bound x largest scale. Especially since this isn't fully accurate anyway: we are getting a @@ -2585,7 +2588,7 @@ IECore::ConstInternedStringVectorDataPtr Instancer::computeBranchChildNames( con ConstEngineSplitPrototypesDataPtr esp = engineSplitPrototypes( sourcePath, context ); - const std::vector &pointIndicesForPrototype = esp->pointIndicesForPrototype( branchPath.back() ); + const std::vector &pointIndicesForPrototype = esp->pointIndicesForPrototype( branchPath.back() ); // The children of the prototypeName are all the instances which use this prototype, // which we can query from the engine - however the names we output under use @@ -2596,7 +2599,7 @@ IECore::ConstInternedStringVectorDataPtr Instancer::computeBranchChildNames( con ids.reserve( pointIndicesForPrototype.size() ); const EngineData *engineData = esp->engine(); - for( int q : pointIndicesForPrototype ) + for( size_t q : pointIndicesForPrototype ) { ids.push_back( engineData->instanceId( q ) ); } @@ -3176,7 +3179,7 @@ void Instancer::InstancerCapsule::render( IECoreScenePreview::Renderer *renderer // If we haven't allocated a name for this prototype index, allocate it now, // including additional storage that will hold the digits for each instance id const std::string &protoName = engines[0]->prototypeNames()->readable()[ protoIndex ].string(); - names[protoIndex].reserve( protoName.size() + std::numeric_limits< int >::digits10 + 1 ); + names[protoIndex].reserve( protoName.size() + std::numeric_limits< int64_t >::digits10 + 1 ); names[protoIndex] += protoName; names[protoIndex].append( 1, '/' ); namePrefixLengths[protoIndex] = names[protoIndex].size(); From 743b9b72b168fa4a3ab283b7c0e7450cd6b65950 Mon Sep 17 00:00:00 2001 From: ivanimanishi Date: Fri, 1 Nov 2024 11:20:10 -0700 Subject: [PATCH 17/34] OpDialogue : Fix postExecuteBehaviour handling This was broken by the switch to native python enums from `IECore.Enum`. This commit also preserves compatibility with the old string values provided by `IECore.Enum`. Note that having the `Ops` refer to `GafferCortexUI.OpDialogue.PostExecuteBehaviour` instead of hard-coded strings is not practical because that would require importing `GafferCortexUI` in the `Op`. --- Changes.md | 1 + python/GafferCortexUI/OpDialogue.py | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Changes.md b/Changes.md index 7aa840169ad..81711d5226c 100644 --- a/Changes.md +++ b/Changes.md @@ -13,6 +13,7 @@ Fixes - Render, InteractiveRender : Added default node name arguments to the compatibility shims for removed subclasses such as ArnoldRender. - GafferUITest : Fixed `assertNodeUIsHaveExpectedLifetime()` test for invisible nodes. +- OpDialogue : Fixed `postExecuteBehaviour` handling. 1.5.0.1 (relative to 1.5.0.0) ======= diff --git a/python/GafferCortexUI/OpDialogue.py b/python/GafferCortexUI/OpDialogue.py index f38b971d242..3991572113c 100644 --- a/python/GafferCortexUI/OpDialogue.py +++ b/python/GafferCortexUI/OpDialogue.py @@ -130,10 +130,16 @@ def __init__( with IECore.IgnoredExceptions( KeyError ) : d = opInstance.userData()["UI"]["postExecuteBehaviour"] if d is not None : - for v in self.PostExecuteBehaviour.values() : + for v in self.PostExecuteBehaviour : if str( v ).lower() == d.value.lower() : postExecuteBehaviour = v break + + # backwards compatibility for IECore.Enum() + if str( v ).lower() == f"PostExecuteBehaviour.{d.value}".lower() : + postExecuteBehaviour = v + break + else : # backwards compatibility with batata with IECore.IgnoredExceptions( KeyError ) : From bae8a2a35d79ae25feb0ff5efafd2b9b1d0b674a Mon Sep 17 00:00:00 2001 From: Daniel Dresser Date: Mon, 21 Oct 2024 17:37:10 -0700 Subject: [PATCH 18/34] TypedObjectPlug : Add Int64VectorDataPlug --- Changes.md | 5 +++++ include/Gaffer/TypeIds.h | 1 + include/Gaffer/TypedObjectPlug.h | 3 +++ python/GafferUI/VectorDataPlugValueWidget.py | 1 + src/Gaffer/PlugAlgo.cpp | 11 +++++++++++ src/Gaffer/TypedObjectPlug.cpp | 2 ++ src/GafferModule/TypedObjectPlugBinding.cpp | 1 + 7 files changed, 24 insertions(+) diff --git a/Changes.md b/Changes.md index 7aa840169ad..12e74014f74 100644 --- a/Changes.md +++ b/Changes.md @@ -14,6 +14,11 @@ Fixes - Render, InteractiveRender : Added default node name arguments to the compatibility shims for removed subclasses such as ArnoldRender. - GafferUITest : Fixed `assertNodeUIsHaveExpectedLifetime()` test for invisible nodes. +API +--- + +- Int64VectorDataPlug : Added new plug type for passing vectors of int64. + 1.5.0.1 (relative to 1.5.0.0) ======= diff --git a/include/Gaffer/TypeIds.h b/include/Gaffer/TypeIds.h index 0692213d61a..1146a382963 100644 --- a/include/Gaffer/TypeIds.h +++ b/include/Gaffer/TypeIds.h @@ -154,6 +154,7 @@ enum TypeId CollectTypeId = 110108, Box2fVectorDataPlugTypeId = 110109, PatternMatchTypeId = 110110, + Int64VectorDataPlugTypeId = 110111, LastTypeId = 110159, diff --git a/include/Gaffer/TypedObjectPlug.h b/include/Gaffer/TypedObjectPlug.h index 50105ddebd3..9b3b10d7ec5 100644 --- a/include/Gaffer/TypedObjectPlug.h +++ b/include/Gaffer/TypedObjectPlug.h @@ -124,6 +124,7 @@ class IECORE_EXPORT TypedObjectPlug : public ValuePlug extern template class TypedObjectPlug; extern template class TypedObjectPlug; extern template class TypedObjectPlug; +extern template class TypedObjectPlug; extern template class TypedObjectPlug; extern template class TypedObjectPlug; extern template class TypedObjectPlug; @@ -146,6 +147,7 @@ extern template class TypedObjectPlug; using ObjectPlug = TypedObjectPlug; using BoolVectorDataPlug = TypedObjectPlug; using IntVectorDataPlug = TypedObjectPlug; +using Int64VectorDataPlug = TypedObjectPlug; using FloatVectorDataPlug = TypedObjectPlug; using StringVectorDataPlug = TypedObjectPlug; using InternedStringVectorDataPlug = TypedObjectPlug; @@ -166,6 +168,7 @@ using PathMatcherDataPlug = TypedObjectPlug; IE_CORE_DECLAREPTR( ObjectPlug ); IE_CORE_DECLAREPTR( BoolVectorDataPlug ); IE_CORE_DECLAREPTR( IntVectorDataPlug ); +IE_CORE_DECLAREPTR( Int64VectorDataPlug ); IE_CORE_DECLAREPTR( FloatVectorDataPlug ); IE_CORE_DECLAREPTR( StringVectorDataPlug ); IE_CORE_DECLAREPTR( InternedStringVectorDataPlug ); diff --git a/python/GafferUI/VectorDataPlugValueWidget.py b/python/GafferUI/VectorDataPlugValueWidget.py index 753e577e00a..72f9d958d56 100644 --- a/python/GafferUI/VectorDataPlugValueWidget.py +++ b/python/GafferUI/VectorDataPlugValueWidget.py @@ -152,6 +152,7 @@ def _createRows( self ) : GafferUI.PlugValueWidget.registerType( Gaffer.BoolVectorDataPlug, VectorDataPlugValueWidget ) GafferUI.PlugValueWidget.registerType( Gaffer.IntVectorDataPlug, VectorDataPlugValueWidget ) +GafferUI.PlugValueWidget.registerType( Gaffer.Int64VectorDataPlug, VectorDataPlugValueWidget ) GafferUI.PlugValueWidget.registerType( Gaffer.FloatVectorDataPlug, VectorDataPlugValueWidget ) GafferUI.PlugValueWidget.registerType( Gaffer.StringVectorDataPlug, VectorDataPlugValueWidget ) GafferUI.PlugValueWidget.registerType( Gaffer.V2iVectorDataPlug, VectorDataPlugValueWidget ) diff --git a/src/Gaffer/PlugAlgo.cpp b/src/Gaffer/PlugAlgo.cpp index c73932b40f6..e9d50423ff3 100644 --- a/src/Gaffer/PlugAlgo.cpp +++ b/src/Gaffer/PlugAlgo.cpp @@ -375,6 +375,10 @@ ValuePlugPtr createPlugFromData( const std::string &name, Plug::Direction direct { return typedObjectValuePlug( name, direction, flags, static_cast( value ) ); } + case Int64VectorDataTypeId : + { + return typedObjectValuePlug( name, direction, flags, static_cast( value ) ); + } case StringVectorDataTypeId : { return typedObjectValuePlug( name, direction, flags, static_cast( value ) ); @@ -488,6 +492,8 @@ IECore::DataPtr getValueAsData( const ValuePlug *plug ) return static_cast( plug )->getValue()->copy(); case IntVectorDataPlugTypeId : return static_cast( plug )->getValue()->copy(); + case Int64VectorDataPlugTypeId : + return static_cast( plug )->getValue()->copy(); case StringVectorDataPlugTypeId : return static_cast( plug )->getValue()->copy(); case InternedStringVectorDataPlugTypeId : @@ -1015,6 +1021,7 @@ bool canSetCompoundNumericPlugValue( const Data *value ) case V2iVectorDataTypeId : case FloatVectorDataTypeId : case IntVectorDataTypeId : + case Int64VectorDataTypeId : case BoolVectorDataTypeId : return IECore::size( value ) == 1; default : @@ -1071,6 +1078,8 @@ bool canSetValueFromData( const ValuePlug *plug, const IECore::Data *value ) return canSetTypedDataPlugValue( value ); case Gaffer::IntVectorDataPlugTypeId: return canSetTypedDataPlugValue( value ); + case Gaffer::Int64VectorDataPlugTypeId: + return canSetTypedDataPlugValue( value ); case Gaffer::StringPlugTypeId: return canSetStringPlugValue( value ); case Gaffer::StringVectorDataPlugTypeId: @@ -1145,6 +1154,8 @@ bool setValueFromData( ValuePlug *plug, const IECore::Data *value ) return setTypedDataPlugValue( static_cast( plug ), value ); case Gaffer::IntVectorDataPlugTypeId: return setTypedDataPlugValue( static_cast( plug ), value ); + case Gaffer::Int64VectorDataPlugTypeId: + return setTypedDataPlugValue( static_cast( plug ), value ); case Gaffer::StringPlugTypeId: return setStringPlugValue( static_cast( plug ), value ); case Gaffer::StringVectorDataPlugTypeId: diff --git a/src/Gaffer/TypedObjectPlug.cpp b/src/Gaffer/TypedObjectPlug.cpp index c8bbfe93aee..1314232924f 100644 --- a/src/Gaffer/TypedObjectPlug.cpp +++ b/src/Gaffer/TypedObjectPlug.cpp @@ -49,6 +49,7 @@ namespace Gaffer GAFFER_PLUG_DEFINE_TEMPLATE_TYPE( Gaffer::ObjectPlug, ObjectPlugTypeId ) GAFFER_PLUG_DEFINE_TEMPLATE_TYPE( Gaffer::BoolVectorDataPlug, BoolVectorDataPlugTypeId ) GAFFER_PLUG_DEFINE_TEMPLATE_TYPE( Gaffer::IntVectorDataPlug, IntVectorDataPlugTypeId ) +GAFFER_PLUG_DEFINE_TEMPLATE_TYPE( Gaffer::Int64VectorDataPlug, Int64VectorDataPlugTypeId ) GAFFER_PLUG_DEFINE_TEMPLATE_TYPE( Gaffer::FloatVectorDataPlug, FloatVectorDataPlugTypeId ) GAFFER_PLUG_DEFINE_TEMPLATE_TYPE( Gaffer::StringVectorDataPlug, StringVectorDataPlugTypeId ) GAFFER_PLUG_DEFINE_TEMPLATE_TYPE( Gaffer::InternedStringVectorDataPlug, InternedStringVectorDataPlugTypeId ) @@ -153,6 +154,7 @@ void StringVectorDataPlug::setFrom( const ValuePlug *other ) template class TypedObjectPlug; template class TypedObjectPlug; template class TypedObjectPlug; +template class TypedObjectPlug; template class TypedObjectPlug; template class TypedObjectPlug; template class TypedObjectPlug; diff --git a/src/GafferModule/TypedObjectPlugBinding.cpp b/src/GafferModule/TypedObjectPlugBinding.cpp index 07a32f36d7d..6f1780bd858 100644 --- a/src/GafferModule/TypedObjectPlugBinding.cpp +++ b/src/GafferModule/TypedObjectPlugBinding.cpp @@ -49,6 +49,7 @@ void GafferModule::bindTypedObjectPlug() GafferBindings::TypedObjectPlugClass(); GafferBindings::TypedObjectPlugClass(); GafferBindings::TypedObjectPlugClass(); + GafferBindings::TypedObjectPlugClass(); GafferBindings::TypedObjectPlugClass(); GafferBindings::TypedObjectPlugClass(); GafferBindings::TypedObjectPlugClass(); From 95af50a8e6c974302dff85cec39fe323a2874ed5 Mon Sep 17 00:00:00 2001 From: Daniel Dresser Date: Tue, 22 Oct 2024 09:41:37 -0700 Subject: [PATCH 19/34] DeletePoints : Add support for id lists. --- Changes.md | 1 + include/GafferScene/DeletePoints.h | 19 ++ python/GafferSceneTest/DeletePointsTest.py | 93 ++++++++ python/GafferSceneUI/DeletePointsUI.py | 51 +++- src/GafferScene/DeletePoints.cpp | 224 ++++++++++++++++-- .../ObjectProcessorBinding.cpp | 11 +- 6 files changed, 381 insertions(+), 18 deletions(-) diff --git a/Changes.md b/Changes.md index 12e74014f74..d05ae72ea75 100644 --- a/Changes.md +++ b/Changes.md @@ -7,6 +7,7 @@ Improvements - Instancer : - Added `inactiveIds` plug for selecting primitive variables to disable some instances. - Added support for 64 bit integer ids (matching what is loaded from USD). +- DeletePoints : Added modes for deleting points based on a list of ids. Fixes ----- diff --git a/include/GafferScene/DeletePoints.h b/include/GafferScene/DeletePoints.h index 12185f22bd9..22449900ae9 100644 --- a/include/GafferScene/DeletePoints.h +++ b/include/GafferScene/DeletePoints.h @@ -54,12 +54,31 @@ class GAFFERSCENE_API DeletePoints : public Deformer public : + enum class SelectionMode + { + VertexPrimitiveVariable, + IdListPrimitiveVariable, + IdList + }; + explicit DeletePoints( const std::string &name = defaultName() ); ~DeletePoints() override; + Gaffer::IntPlug *selectionModePlug(); + const Gaffer::IntPlug *selectionModePlug() const; + Gaffer::StringPlug *pointsPlug(); const Gaffer::StringPlug *pointsPlug() const; + Gaffer::StringPlug *idListVariablePlug(); + const Gaffer::StringPlug *idListVariablePlug() const; + + Gaffer::Int64VectorDataPlug *idListPlug(); + const Gaffer::Int64VectorDataPlug *idListPlug() const; + + Gaffer::StringPlug *idPlug(); + const Gaffer::StringPlug *idPlug() const; + Gaffer::BoolPlug *invertPlug(); const Gaffer::BoolPlug *invertPlug() const; diff --git a/python/GafferSceneTest/DeletePointsTest.py b/python/GafferSceneTest/DeletePointsTest.py index 116ac4bc4c1..cb8576e9476 100644 --- a/python/GafferSceneTest/DeletePointsTest.py +++ b/python/GafferSceneTest/DeletePointsTest.py @@ -149,6 +149,99 @@ def testBoundsUpdate( self ) : self.assertEqual( actualPointsDeletedBounds, expectedBoundingBox ) + def testIdList( self ) : + + testObject = IECoreScene.PointsPrimitive( + IECore.V3fVectorData( [ imath.V3f( 0, 0, i ) for i in range( 10 ) ] ), + IECore.FloatVectorData( range( 10, 20 ) ) + ) + + testObject["testA"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.IntVectorData( [ 2, 3, 4, 8, 9 ] ) ) + testObject["testB"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.Int64VectorData( [ 0, 1, 5, 6, 7 ] ) ) + testObject["shiftIds"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.Int64VectorData( [ i + 5 for i in range( 10 ) ] ) ) + testObject["outOfRangeIds"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.Int64VectorData( [ i + 8000000000 for i in range( 10 ) ] ) ) + testObject["duplicateIds"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.Int64VectorData( [ 0, 0, 1, 1, 2, 2, 3, 3, 4, 4 ] ) ) + + self.assertTrue( testObject.arePrimitiveVariablesValid() ) + + pointsScene = GafferScene.ObjectToScene() + pointsScene["object"].setValue( testObject ) + + deletePoints = GafferScene.DeletePoints() + + deletePoints["in"].setInput( pointsScene["out"] ) + + pathFilter = GafferScene.PathFilter( "PathFilter" ) + pathFilter["paths"].setValue( IECore.StringVectorData( [ '/object' ] ) ) + deletePoints["filter"].setInput( pathFilter["out"] ) + + deletePoints["selectionMode"].setValue( GafferScene.DeletePoints.SelectionMode.IdList ) + deletePoints["idList"].setValue( IECore.Int64VectorData( [ 1, 7 ] ) ) + + self.assertEqual( + deletePoints["out"].object( "/object" )["r"].data, + IECore.FloatVectorData( [ 10, 12, 13, 14, 15, 16, 18, 19 ] ) + ) + + deletePoints["invert"].setValue( True ) + self.assertEqual( + deletePoints["out"].object( "/object" )["r"].data, + IECore.FloatVectorData( [ 11, 17 ] ) + ) + + deletePoints["invert"].setValue( False ) + deletePoints["selectionMode"].setValue( GafferScene.DeletePoints.SelectionMode.IdListPrimitiveVariable ) + deletePoints["idListVariable"].setValue( "testA" ) + + self.assertEqual( + deletePoints["out"].object( "/object" )["r"].data, + IECore.FloatVectorData( [ 10, 11, 15, 16, 17 ] ) + ) + + deletePoints["idListVariable"].setValue( "testB" ) + + self.assertEqual( + deletePoints["out"].object( "/object" )["r"].data, + IECore.FloatVectorData( [ 12, 13, 14, 18, 19 ] ) + ) + + deletePoints["invert"].setValue( True ) + + self.assertEqual( + deletePoints["out"].object( "/object" )["r"].data, + IECore.FloatVectorData( [ 10, 11, 15, 16, 17 ] ) + ) + + deletePoints["invert"].setValue( False ) + + deletePoints["id"].setValue( "shiftIds" ) + + self.assertEqual( + deletePoints["out"].object( "/object" )["r"].data, + IECore.FloatVectorData( [ 13, 14, 15, 16, 17, 18, 19 ] ) + ) + + # Test that we work with ids outside the range of an Int32 + deletePoints["id"].setValue( "outOfRangeIds" ) + + deletePoints["selectionMode"].setValue( GafferScene.DeletePoints.SelectionMode.IdList ) + deletePoints["idList"].setValue( IECore.Int64VectorData( [ 8000000001, 8000000002, 8000000007, 8000000008 ] ) ) + + self.assertEqual( + deletePoints["out"].object( "/object" )["r"].data, + IECore.FloatVectorData( [ 10, 13, 14, 15, 16, 19 ] ) + ) + + # If multiple point have duplicate ids matching a specified id, we delete all copies of the id. + deletePoints["id"].setValue( "duplicateIds" ) + deletePoints["idList"].setValue( IECore.Int64VectorData( [ 0, 2, 4 ] ) ) + + self.assertEqual( + deletePoints["out"].object( "/object" )["r"].data, + IECore.FloatVectorData( [ 12, 13, 16, 17 ] ) + ) + + def testIgnoreMissing( self ) : pointsScene = self.makePoints() diff --git a/python/GafferSceneUI/DeletePointsUI.py b/python/GafferSceneUI/DeletePointsUI.py index da60ae496ff..41d623c7df3 100644 --- a/python/GafferSceneUI/DeletePointsUI.py +++ b/python/GafferSceneUI/DeletePointsUI.py @@ -43,10 +43,26 @@ "description", """ - Deletes points from a points primitive using a primitive variable to choose the points. + Deletes points from a points primitive using a primitive variable or id list to choose the points. """, plugs = { + "selectionMode" : [ + "description", + """ + Chooses how to select points to delete. + + - VertexPrimitiveVariable : Deletes points with a non-zero value in the `points` primitive variable. + - IdListPrimitiveVariable : Deletes points with Ids in the `idListVariable` primitive variable. + - IdList : Deletes points with Ids in the `idList`. + """, + "preset:Vertex Primitive Variable", GafferScene.DeletePoints.SelectionMode.VertexPrimitiveVariable, + "preset:Id List Primitive Variable", GafferScene.DeletePoints.SelectionMode.IdListPrimitiveVariable, + "preset:Id List", GafferScene.DeletePoints.SelectionMode.IdList, + + "plugValueWidget:type", "GafferUI.PresetsPlugValueWidget", + ], + "adjustBounds" : [ @@ -57,10 +73,41 @@ "points" : [ "description", """ - Vertex interpolated int, float or bool primitive variable to choose which points to delete. Note a non-zero value indicates the point will be deleted. + Vertex interpolated int, float or bool primitive variable to choose which points to delete. Note a non-zero value indicates the point will be deleted. Only used when `selectionMode` is "VertexPrimitiveVariable". + """, + + "layout:visibilityActivator", lambda plug : plug.node()["selectionMode"].getValue() == GafferScene.DeletePoints.SelectionMode.VertexPrimitiveVariable + ], + + "idListVariable" : [ + "description", + """ + The name of a constant primitive variable holding a list of ids to delete. Must be type IntVectorData or Int64VectorData. Only used when `selectionMode` is "IdListPrimitiveVariable". + """, + + "layout:visibilityActivator", lambda plug : plug.node()["selectionMode"].getValue() == GafferScene.DeletePoints.SelectionMode.IdListPrimitiveVariable + ], + + "idList" : [ + "description", """ + A list of ids to delete. Only used when `selectionMode` is "IdList". + """, + + "layout:visibilityActivator", lambda plug : plug.node()["selectionMode"].getValue() == GafferScene.DeletePoints.SelectionMode.IdList ], + "id" : [ + "description", + """ + When using an id list to delete points, this primitive variable defines the id used for each point. + If this primitive variable is not found, then the index of each point is its id. + """, + + "layout:visibilityActivator", lambda plug : plug.node()["selectionMode"].getValue() in [ GafferScene.DeletePoints.SelectionMode.IdList, GafferScene.DeletePoints.SelectionMode.IdListPrimitiveVariable ] + ], + + "invert" : [ "description", """ diff --git a/src/GafferScene/DeletePoints.cpp b/src/GafferScene/DeletePoints.cpp index 5e7d6891d13..e72d8a8399a 100644 --- a/src/GafferScene/DeletePoints.cpp +++ b/src/GafferScene/DeletePoints.cpp @@ -46,11 +46,74 @@ #include "fmt/format.h" +#include + using namespace IECore; using namespace IECoreScene; using namespace Gaffer; using namespace GafferScene; +namespace { + +// Copied from Instancer.cpp - maybe should be shared somehow if it gets reused? +struct IdData +{ + IdData() : + intElements( nullptr ), int64Elements( nullptr ) + { + } + + void initialize( const Primitive *primitive, const std::string &name, bool throwIfMissing = false ) + { + if( const IntVectorData *intData = primitive->variableData( name ) ) + { + intElements = &intData->readable(); + } + else if( const Int64VectorData *int64Data = primitive->variableData( name ) ) + { + int64Elements = &int64Data->readable(); + } + else if( throwIfMissing ) + { + throw IECore::Exception( fmt::format( "DeletePoints : No primitive variable \"{}\" found of type IntVectorData or type Int64VectorData", name ) ); + } + } + + size_t size() const + { + if( intElements ) + { + return intElements->size(); + } + else if( int64Elements ) + { + return int64Elements->size(); + } + else + { + return 0; + } + } + + int64_t element( size_t i ) const + { + if( intElements ) + { + return (*intElements)[i]; + } + else + { + return (*int64Elements)[i]; + } + } + + const std::vector *intElements; + const std::vector *int64Elements; + +}; + +} // namespace + GAFFER_NODE_DEFINE_TYPE( DeletePoints ); size_t DeletePoints::g_firstPlugIndex = 0; @@ -60,7 +123,15 @@ DeletePoints::DeletePoints( const std::string &name ) { storeIndexOfNextChild( g_firstPlugIndex ); + addChild(new IntPlug( + "selectionMode", Plug::In, + (int)SelectionMode::VertexPrimitiveVariable, (int)SelectionMode::VertexPrimitiveVariable, (int)SelectionMode::IdList + ) ); addChild( new StringPlug( "points", Plug::In, "deletePoints" ) ); + addChild( new StringPlug( "idListVariable", Plug::In, "inactiveIds" ) ); + addChild( new Int64VectorDataPlug( "idList", Plug::In ) ); + addChild( new StringPlug( "id", Plug::In, "instanceId" ) ); + addChild( new BoolPlug( "invert", Plug::In, false ) ); addChild( new BoolPlug( "ignoreMissingVariable", Plug::In, false ) ); } @@ -69,41 +140,85 @@ DeletePoints::~DeletePoints() { } +Gaffer::IntPlug *DeletePoints::selectionModePlug() +{ + return getChild( g_firstPlugIndex ); +} + +const Gaffer::IntPlug *DeletePoints::selectionModePlug() const +{ + return getChild( g_firstPlugIndex ); +} + Gaffer::StringPlug *DeletePoints::pointsPlug() { - return getChild( g_firstPlugIndex ); + return getChild( g_firstPlugIndex + 1 ); } const Gaffer::StringPlug *DeletePoints::pointsPlug() const { - return getChild( g_firstPlugIndex ); + return getChild( g_firstPlugIndex + 1 ); +} + +Gaffer::StringPlug *DeletePoints::idListVariablePlug() +{ + return getChild( g_firstPlugIndex + 2 ); +} + +const Gaffer::StringPlug *DeletePoints::idListVariablePlug() const +{ + return getChild( g_firstPlugIndex + 2 ); +} + +Gaffer::Int64VectorDataPlug *DeletePoints::idListPlug() +{ + return getChild( g_firstPlugIndex + 3 ); +} + +const Gaffer::Int64VectorDataPlug *DeletePoints::idListPlug() const +{ + return getChild( g_firstPlugIndex + 3 ); +} + +Gaffer::StringPlug *DeletePoints::idPlug() +{ + return getChild( g_firstPlugIndex + 4 ); +} + +const Gaffer::StringPlug *DeletePoints::idPlug() const +{ + return getChild( g_firstPlugIndex + 4 ); } Gaffer::BoolPlug *DeletePoints::invertPlug() { - return getChild( g_firstPlugIndex + 1); + return getChild( g_firstPlugIndex + 5 ); } const Gaffer::BoolPlug *DeletePoints::invertPlug() const { - return getChild( g_firstPlugIndex + 1); + return getChild( g_firstPlugIndex + 5 ); } Gaffer::BoolPlug *DeletePoints::ignoreMissingVariablePlug() { - return getChild( g_firstPlugIndex + 2 ); + return getChild( g_firstPlugIndex + 6 ); } const Gaffer::BoolPlug *DeletePoints::ignoreMissingVariablePlug() const { - return getChild( g_firstPlugIndex + 2 ); + return getChild( g_firstPlugIndex + 6 ); } bool DeletePoints::affectsProcessedObject( const Gaffer::Plug *input ) const { return Deformer::affectsProcessedObject( input ) || + input == selectionModePlug() || input == pointsPlug() || + input == idListVariablePlug() || + input == idListPlug() || + input == idPlug() || input == invertPlug() || input == ignoreMissingVariablePlug() ; @@ -112,7 +227,11 @@ bool DeletePoints::affectsProcessedObject( const Gaffer::Plug *input ) const void DeletePoints::hashProcessedObject( const ScenePath &path, const Gaffer::Context *context, IECore::MurmurHash &h ) const { Deformer::hashProcessedObject( path, context, h ); + selectionModePlug()->hash( h ); pointsPlug()->hash( h ); + idListVariablePlug()->hash( h ); + idListPlug()->hash( h ); + idPlug()->hash( h ); invertPlug()->hash( h ); ignoreMissingVariablePlug()->hash( h ); } @@ -125,23 +244,98 @@ IECore::ConstObjectPtr DeletePoints::computeProcessedObject( const ScenePath &pa return inputObject; } - std::string deletePrimVarName = pointsPlug()->getValue(); + SelectionMode selectionMode = (SelectionMode)selectionModePlug()->getValue(); + + IECoreScene::PrimitiveVariable toDelete; - if( deletePrimVarName.empty() ) + if( selectionMode == SelectionMode::VertexPrimitiveVariable ) { - return inputObject; + std::string deletePrimVarName = pointsPlug()->getValue(); + + if( deletePrimVarName.empty() ) + { + return inputObject; + } + + PrimitiveVariableMap::const_iterator it = points->variables.find( deletePrimVarName ); + if( it == points->variables.end() ) + { + if( ignoreMissingVariablePlug()->getValue() ) + { + return inputObject; + } + + throw InvalidArgumentException( fmt::format( "DeletePoints : No primitive variable \"{}\" found", deletePrimVarName ) ); + } + + toDelete = it->second; } - PrimitiveVariableMap::const_iterator it = points->variables.find( deletePrimVarName ); - if( it == points->variables.end() ) + + + if( selectionMode == SelectionMode::IdListPrimitiveVariable || selectionMode == SelectionMode::IdList ) { - if( ignoreMissingVariablePlug()->getValue() ) + IdData idList; + ConstInt64VectorDataPtr idListData; + + if( selectionMode == SelectionMode::IdListPrimitiveVariable ) { - return inputObject; + std::string idListVarName = idListVariablePlug()->getValue(); + + if( idListVarName.empty() ) + { + return inputObject; + } + + idList.initialize( points, idListVarName, /* throwIfMissing = */ true ); + } + else + { + idListData = idListPlug()->getValue(); + idList.int64Elements = &idListData->readable(); } - throw InvalidArgumentException( fmt::format( "DeletePoints : No primitive variable \"{}\" found", deletePrimVarName ) ); + + IdData ids; + ids.initialize( points, idPlug()->getValue() ); + + size_t numPoints = points->getNumPoints(); + size_t numIds = idList.size(); + + BoolVectorDataPtr inactiveData = new BoolVectorData(); + std::vector &inactive = inactiveData->writable(); + inactive.resize( numPoints, false ); + + + if( ids.size() ) + { + std::unordered_set< int64_t > idSet; + + for( size_t i = 0; i < numIds; i++ ) + { + idSet.insert( idList.element( i ) ); + + } + + for( size_t j = 0; j < numPoints; j++ ) + { + if( idSet.count( ids.element( j ) ) ) + { + inactive[ j ] = true; + } + } + } + else + { + for( size_t i = 0; i < numIds; i++ ) + { + inactive[ idList.element(i) ] = true; + } + } + + toDelete = IECoreScene::PrimitiveVariable( PrimitiveVariable::Interpolation::Vertex, inactiveData ); } - return PointsAlgo::deletePoints( points, it->second, invertPlug()->getValue() ); + + return PointsAlgo::deletePoints( points, toDelete, invertPlug()->getValue() ); } diff --git a/src/GafferSceneModule/ObjectProcessorBinding.cpp b/src/GafferSceneModule/ObjectProcessorBinding.cpp index 85c78e4f303..4d6adbeee2c 100644 --- a/src/GafferSceneModule/ObjectProcessorBinding.cpp +++ b/src/GafferSceneModule/ObjectProcessorBinding.cpp @@ -76,7 +76,6 @@ void GafferSceneModule::bindObjectProcessor() GafferBindings::DependencyNodeClass(); GafferBindings::DependencyNodeClass(); - GafferBindings::DependencyNodeClass(); GafferBindings::DependencyNodeClass(); GafferBindings::DependencyNodeClass(); GafferBindings::DependencyNodeClass(); @@ -98,6 +97,16 @@ void GafferSceneModule::bindObjectProcessor() GafferBindings::DependencyNodeClass(); GafferBindings::DependencyNodeClass(); + { + scope s = GafferBindings::DependencyNodeClass(); + + enum_( "SelectionMode" ) + .value( "VertexPrimitiveVariable", GafferScene::DeletePoints::SelectionMode::VertexPrimitiveVariable ) + .value( "IdListPrimitiveVariable", GafferScene::DeletePoints::SelectionMode::IdListPrimitiveVariable ) + .value( "IdList", GafferScene::DeletePoints::SelectionMode::IdList ) + ; + } + { scope s = GafferBindings::DependencyNodeClass(); From 72f986a287daae623929ecc15fbafb3a3a75e70d Mon Sep 17 00:00:00 2001 From: Daniel Dresser Date: Mon, 28 Oct 2024 17:38:23 -0700 Subject: [PATCH 20/34] DeletePointsTest : Avoid use of deprecated "r" primvar --- python/GafferSceneTest/DeletePointsTest.py | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/python/GafferSceneTest/DeletePointsTest.py b/python/GafferSceneTest/DeletePointsTest.py index cb8576e9476..1bb01b9b4af 100644 --- a/python/GafferSceneTest/DeletePointsTest.py +++ b/python/GafferSceneTest/DeletePointsTest.py @@ -54,10 +54,10 @@ def makePoints( self ) : imath.V3f( 1, 0, 0 ) ] - ), - IECore.FloatVectorData( range( 0, 4 ) ) + ) ) + testObject["q"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.FloatVectorData( range( 0, 4 ) ) ) testObject["deletePoints"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.IntVectorData( [0, 1, 0, 1] ) ) testObject["deletePoints2"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.IntVectorData( [1, 1, 0, 0] ) ) @@ -91,7 +91,7 @@ def testCanDeletePoints( self ) : #IECore.V3f( 1, 0, 0 ) ], IECore.GeometricData.Interpretation.Point ) ) - self.assertEqual( pointsDeletedObject["r"].data, IECore.FloatVectorData( + self.assertEqual( pointsDeletedObject["q"].data, IECore.FloatVectorData( [ 0, 2 ] ) ) @@ -117,7 +117,7 @@ def testCanDeletePoints( self ) : imath.V3f( 1, 0, 0 ) ], IECore.GeometricData.Interpretation.Point ) ) - self.assertEqual( pointsDeletedObject["r"].data, IECore.FloatVectorData( + self.assertEqual( pointsDeletedObject["q"].data, IECore.FloatVectorData( [ 1, 3 ] ) ) @@ -152,9 +152,9 @@ def testBoundsUpdate( self ) : def testIdList( self ) : testObject = IECoreScene.PointsPrimitive( - IECore.V3fVectorData( [ imath.V3f( 0, 0, i ) for i in range( 10 ) ] ), - IECore.FloatVectorData( range( 10, 20 ) ) + IECore.V3fVectorData( [ imath.V3f( 0, 0, i ) for i in range( 10 ) ] ) ) + testObject["q"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.FloatVectorData( range( 10, 20 ) ) ) testObject["testA"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.IntVectorData( [ 2, 3, 4, 8, 9 ] ) ) testObject["testB"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.Int64VectorData( [ 0, 1, 5, 6, 7 ] ) ) @@ -179,13 +179,13 @@ def testIdList( self ) : deletePoints["idList"].setValue( IECore.Int64VectorData( [ 1, 7 ] ) ) self.assertEqual( - deletePoints["out"].object( "/object" )["r"].data, + deletePoints["out"].object( "/object" )["q"].data, IECore.FloatVectorData( [ 10, 12, 13, 14, 15, 16, 18, 19 ] ) ) deletePoints["invert"].setValue( True ) self.assertEqual( - deletePoints["out"].object( "/object" )["r"].data, + deletePoints["out"].object( "/object" )["q"].data, IECore.FloatVectorData( [ 11, 17 ] ) ) @@ -194,21 +194,21 @@ def testIdList( self ) : deletePoints["idListVariable"].setValue( "testA" ) self.assertEqual( - deletePoints["out"].object( "/object" )["r"].data, + deletePoints["out"].object( "/object" )["q"].data, IECore.FloatVectorData( [ 10, 11, 15, 16, 17 ] ) ) deletePoints["idListVariable"].setValue( "testB" ) self.assertEqual( - deletePoints["out"].object( "/object" )["r"].data, + deletePoints["out"].object( "/object" )["q"].data, IECore.FloatVectorData( [ 12, 13, 14, 18, 19 ] ) ) deletePoints["invert"].setValue( True ) self.assertEqual( - deletePoints["out"].object( "/object" )["r"].data, + deletePoints["out"].object( "/object" )["q"].data, IECore.FloatVectorData( [ 10, 11, 15, 16, 17 ] ) ) @@ -217,7 +217,7 @@ def testIdList( self ) : deletePoints["id"].setValue( "shiftIds" ) self.assertEqual( - deletePoints["out"].object( "/object" )["r"].data, + deletePoints["out"].object( "/object" )["q"].data, IECore.FloatVectorData( [ 13, 14, 15, 16, 17, 18, 19 ] ) ) @@ -228,7 +228,7 @@ def testIdList( self ) : deletePoints["idList"].setValue( IECore.Int64VectorData( [ 8000000001, 8000000002, 8000000007, 8000000008 ] ) ) self.assertEqual( - deletePoints["out"].object( "/object" )["r"].data, + deletePoints["out"].object( "/object" )["q"].data, IECore.FloatVectorData( [ 10, 13, 14, 15, 16, 19 ] ) ) @@ -237,7 +237,7 @@ def testIdList( self ) : deletePoints["idList"].setValue( IECore.Int64VectorData( [ 0, 2, 4 ] ) ) self.assertEqual( - deletePoints["out"].object( "/object" )["r"].data, + deletePoints["out"].object( "/object" )["q"].data, IECore.FloatVectorData( [ 12, 13, 16, 17 ] ) ) From 774eea8a6d4699c21f66024310cb6755fcf68217 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Wed, 25 Sep 2024 12:15:42 -0400 Subject: [PATCH 21/34] PlugPopup : Add original / current color swatches --- Changes.md | 1 + python/GafferUI/ColorChooser.py | 2 +- .../GafferUI/ColorChooserPlugValueWidget.py | 16 ++++++++++++ python/GafferUI/ColorPlugValueWidget.py | 16 ++++++++++++ python/GafferUI/PlugPopup.py | 26 +++++++++++++++++++ 5 files changed, 60 insertions(+), 1 deletion(-) diff --git a/Changes.md b/Changes.md index 894cc9ab84c..6639b2ef17c 100644 --- a/Changes.md +++ b/Changes.md @@ -8,6 +8,7 @@ Improvements - Added `inactiveIds` plug for selecting primitive variables to disable some instances. - Added support for 64 bit integer ids (matching what is loaded from USD). - DeletePoints : Added modes for deleting points based on a list of ids. +- Light Editor, Attribute Editor, Spreadsheet : Add original and current color swatches to color popups. Fixes ----- diff --git a/python/GafferUI/ColorChooser.py b/python/GafferUI/ColorChooser.py index 87d487986b1..a4ff0e0f10a 100644 --- a/python/GafferUI/ColorChooser.py +++ b/python/GafferUI/ColorChooser.py @@ -866,7 +866,7 @@ def getColor( self ) : def setSwatchesVisible( self, visible ) : - self.__swatchRow.setVisible( False ) + self.__swatchRow.setVisible( visible ) def getSwatchesVisible( self ) : diff --git a/python/GafferUI/ColorChooserPlugValueWidget.py b/python/GafferUI/ColorChooserPlugValueWidget.py index 9076079cd1c..a7e2a2fffc3 100644 --- a/python/GafferUI/ColorChooserPlugValueWidget.py +++ b/python/GafferUI/ColorChooserPlugValueWidget.py @@ -86,6 +86,22 @@ def __init__( self, plugs, **kw ) : self.__lastChangedReason = None self.__mergeGroupId = 0 + def setInitialColor( self, color ) : + + self.__colorChooser.setInitialColor( color ) + + def getInitialColor( self ) : + + return self.__colorChooser.getInitialColor() + + def setSwatchesVisible( self, visible ) : + + self.__colorChooser.setSwatchesVisible( visible ) + + def getSwatchesVisible( self ) : + + return self.__colorChooser.getVisible() + def _updateFromValues( self, values, exception ) : # ColorChooser only supports one colour, and doesn't have diff --git a/python/GafferUI/ColorPlugValueWidget.py b/python/GafferUI/ColorPlugValueWidget.py index 1386efb3943..819e4f626ce 100644 --- a/python/GafferUI/ColorPlugValueWidget.py +++ b/python/GafferUI/ColorPlugValueWidget.py @@ -96,6 +96,22 @@ def getColorChooserVisible( self ) : return self.__colorChooser.getVisible() if self.__colorChooser is not None else False + def setInitialColor( self, color ) : + + self.__colorChooser.setInitialColor( color ) + + def getInitialColor( self ) : + + return self.__colorChooser.getInitialColor() + + def setSwatchesVisible( self, visible ) : + + self.__colorChooser.setSwatchesVisible( visible ) + + def getSwatchesVisible( self ) : + + return self.__colorChooser.getVisible() + def setPlugs( self, plugs ) : GafferUI.PlugValueWidget.setPlugs( self, plugs ) diff --git a/python/GafferUI/PlugPopup.py b/python/GafferUI/PlugPopup.py index 6a1432a4289..062b6d024a3 100644 --- a/python/GafferUI/PlugPopup.py +++ b/python/GafferUI/PlugPopup.py @@ -34,6 +34,8 @@ # ########################################################################## +import imath + import Gaffer import GafferUI @@ -122,6 +124,30 @@ def popup( self, center = None, parent = None ) : GafferUI.PopupWindow.popup( self, center, parent ) + colorPlugValueWidget = self.__colorPlugValueWidget( self.__plugValueWidget ) + if colorPlugValueWidget is not None and len( self.__plugValueWidget.getPlugs() ) > 0 : + colors = [ + p.getValue() for p in self.__plugValueWidget.getPlugs() if ( + isinstance( p, Gaffer.Color3fPlug ) or isinstance( p, Gaffer.Color4fPlug ) + ) + ] + if len( colors ) == 0 : + for c in self.__plugValueWidget.getPlugs() : + colors += [ p.getValue() for p in Gaffer.Color3fPlug.RecursiveRange( c ) ] + if len( colors ) == 0 : + for c in self.__plugValueWidget.getPlugs() : + colors += [ p.getValue() for p in Gaffer.Color4fPlug.RecursiveRange( c ) ] + + assert( len( colors ) > 0 ) + assert( + all( + isinstance( c, imath.Color3f ) for c in colors + ) or all( isinstance( c, imath.Color4f ) for c in colors ) + ) + + colorPlugValueWidget.setInitialColor( sum( colors ) / len( colors ) ) + colorPlugValueWidget.setSwatchesVisible( True ) + # Attempt to focus the first text widget. This is done after making # the window visible, as we check child widget visibility to avoid # attempting to focus hidden widgets. From 370214df2d548cc783ca1290b0520c33d998f8fc Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Fri, 8 Nov 2024 11:40:16 -0500 Subject: [PATCH 22/34] SceneView : Prevent empty framing bound. --- Changes.md | 1 + src/GafferSceneUI/SceneView.cpp | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/Changes.md b/Changes.md index 6639b2ef17c..4ceeeb9ce46 100644 --- a/Changes.md +++ b/Changes.md @@ -9,6 +9,7 @@ Improvements - Added support for 64 bit integer ids (matching what is loaded from USD). - DeletePoints : Added modes for deleting points based on a list of ids. - Light Editor, Attribute Editor, Spreadsheet : Add original and current color swatches to color popups. +- SceneView : Added fallback framing extents to create a reasonable view when `SceneGadget` is empty, for example if the grid is hidden. Fixes ----- diff --git a/src/GafferSceneUI/SceneView.cpp b/src/GafferSceneUI/SceneView.cpp index 6d9a57e112e..e015fd7f39e 100644 --- a/src/GafferSceneUI/SceneView.cpp +++ b/src/GafferSceneUI/SceneView.cpp @@ -2144,6 +2144,11 @@ Imath::Box3f SceneView::framingBound() const gridGadget->waitForCompletion(); b.extendBy( gridGadget->bound() ); } + + if( b.isEmpty() ) + { + b.extendBy( Imath::Box3f( Imath::V3f( -5.f, 0.f, -5.f ), Imath::V3f( 5.f, 0.f, 5.f ) ) ); + } } return b; From 963c1eb58724f9663b0d32c633c0176498035c45 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Fri, 8 Nov 2024 09:36:40 +0000 Subject: [PATCH 23/34] LocalDispatcher : Fix status update in `Job.kill()` This occurred only when `kill()` was called so quickly after dispatch that the background task hadn't even started. Because we were botching the status update, `LocalDispatcherTest.testKill()` could hang indefinitely waiting for the killed job to finish. This wasn't happening on CI for some reason, but was reproducible reliably on a local machine with more cores. Analysis and proposal for the fix is courtesy of Ivan Imanishi - I'm just making the PR because I'm not trapped behind a firewall. --- Changes.md | 1 + python/GafferDispatch/LocalDispatcher.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Changes.md b/Changes.md index 4ceeeb9ce46..aa0d28def5a 100644 --- a/Changes.md +++ b/Changes.md @@ -17,6 +17,7 @@ Fixes - Render, InteractiveRender : Added default node name arguments to the compatibility shims for removed subclasses such as ArnoldRender. - GafferUITest : Fixed `assertNodeUIsHaveExpectedLifetime()` test for invisible nodes. - OpDialogue : Fixed `postExecuteBehaviour` handling. +- LocalDispatcher : Fixed job status update when a job was killed _immediately_ after being launched. API --- diff --git a/python/GafferDispatch/LocalDispatcher.py b/python/GafferDispatch/LocalDispatcher.py index 388a731beeb..85eef6f9c3e 100644 --- a/python/GafferDispatch/LocalDispatcher.py +++ b/python/GafferDispatch/LocalDispatcher.py @@ -181,16 +181,17 @@ def kill( self ) : if self.__backgroundTask is not None : self.__backgroundTask.cancel() - if self.__backgroundTask.status() == self.__backgroundTask.Status.Pending : + if self.__backgroundTask.status() == self.__backgroundTask.Status.Cancelled : # Usually we'll get to update our status naturally because `__executeInternal()` # will throw `IECore.Cancelled.`. But if `__executeInternal()` hasn't been called - # by the BackgroundTask yet, then it will _never_ be called. We work around this by + # by the BackgroundTask yet, then it will _never_ be called, and the BackgroundTask + # will have transitioned to a Cancelled status _immediately_. We work around this by # updating status manually. # # In many ways it would be great if we used `__backgroundTask.status()` directly # as _our_ status. But that can't work for foreground dispatches, so for the moment # we prefer to track foreground/background status identically. - self.__updateState( self.Status.Killed ) + self.__updateStatus( self.Status.Killed ) def statusChangedSignal( self ) : From ac54e015fbadda099cbc88e849c50f1277795f45 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Mon, 11 Nov 2024 11:09:17 +0000 Subject: [PATCH 24/34] OpenColorIOConfigPlugUI : Fix connection to scriptless apps We were missing the `__default__` display transform registration for application that didn't have a ScriptNode. This meant that the default transform was broken in `gaffer view`. --- Changes.md | 4 ++++ .../GafferImageUI/OpenColorIOConfigPlugUI.py | 24 ++++++++++++++----- startup/gui/ocio.py | 6 +---- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/Changes.md b/Changes.md index aa0d28def5a..bd149aab715 100644 --- a/Changes.md +++ b/Changes.md @@ -18,11 +18,15 @@ Fixes - GafferUITest : Fixed `assertNodeUIsHaveExpectedLifetime()` test for invisible nodes. - OpDialogue : Fixed `postExecuteBehaviour` handling. - LocalDispatcher : Fixed job status update when a job was killed _immediately_ after being launched. +- `gaffer view` : Fixed default OpenColorIO display transform. API --- - Int64VectorDataPlug : Added new plug type for passing vectors of int64. +- OpenColorIOConfigPlugUI : + - Added `connectToApplication()` function. + - Deprecated `connect()` function. Use `connectToApplication()` instead. 1.5.0.1 (relative to 1.5.0.0) ======= diff --git a/python/GafferImageUI/OpenColorIOConfigPlugUI.py b/python/GafferImageUI/OpenColorIOConfigPlugUI.py index 17b4be599df..bd433a0f447 100644 --- a/python/GafferImageUI/OpenColorIOConfigPlugUI.py +++ b/python/GafferImageUI/OpenColorIOConfigPlugUI.py @@ -287,20 +287,32 @@ def __setToDefault( self, *unused ) : for plug in self.getPlugs() : self.getPlug().setValue( "__default__" ) -# Connection between default script config and Widget and View display transforms. -# Calling `connect()` from an application startup file is what makes the UI OpenColorIO-aware. +# Connection between application script configs and Widget and View display +# transforms. Calling `connectToApplication()` from an application startup file +# is what makes the UI OpenColorIO-aware. +def connectToApplication( application ) : + GafferUI.View.DisplayTransform.registerDisplayTransform( + "__default__", __defaultViewDisplayTransformCreator + ) + + application.root()["scripts"].childAddedSignal().connect( __scriptAdded ) + +## \deprecated. Use `connectToApplication()` instead. def connect( script ) : + GafferUI.View.DisplayTransform.registerDisplayTransform( + "__default__", __defaultViewDisplayTransformCreator + ) + __scriptAdded( script.parent(), script ) + +def __scriptAdded( container, script ) : + hadPlug = GafferImage.OpenColorIOConfigPlug.acquireDefaultConfigPlug( script, createIfNecessary = False ) plug = GafferImage.OpenColorIOConfigPlug.acquireDefaultConfigPlug( script ) if not hadPlug : Gaffer.NodeAlgo.applyUserDefaults( plug ) - GafferUI.View.DisplayTransform.registerDisplayTransform( - "__default__", __defaultViewDisplayTransformCreator - ) - script.plugDirtiedSignal().connect( __scriptPlugDirtied ) __scriptPlugDirtied( plug ) diff --git a/startup/gui/ocio.py b/startup/gui/ocio.py index 046e272a42e..4d7c6724cfa 100644 --- a/startup/gui/ocio.py +++ b/startup/gui/ocio.py @@ -45,11 +45,7 @@ # Make sure every script has a config plug added to it, and that we update # the View and Widget display transforms appropriately when the config is changed. -def __scriptAdded( container, script ) : - - GafferImageUI.OpenColorIOConfigPlugUI.connect( script ) - -application.root()["scripts"].childAddedSignal().connect( __scriptAdded ) +GafferImageUI.OpenColorIOConfigPlugUI.connectToApplication( application ) Gaffer.Metadata.registerValue( GafferUI.View, "displayTransform.name", "plugValueWidget:type", "GafferImageUI.OpenColorIOConfigPlugUI.DisplayTransformPlugValueWidget" ) Gaffer.Metadata.registerValue( GafferUI.View, "displayTransform.name", "layout:minimumWidth", 150 ) From 5bc6a6141a8decbb19a8150d0086dd0b1a3e168f Mon Sep 17 00:00:00 2001 From: John Haddon Date: Tue, 5 Nov 2024 12:28:59 +0000 Subject: [PATCH 25/34] AnimationEditor : Fix changing of frame by click & drag We mustn't edit the context returned by the ContextTracker - nobody is tracking it for changes, it is intended to be const, and other UI components might be using it in other threads. Instead we just use the ScriptNode context in the AnimationEditor as before. This is the most minimal change needed to fix the bug. You could argue the case for making the AnimationEditor genuinely focus-aware by accounting for TimeWarps between the node being viewed and the node being edited. That would require a lot of work though, and it's not clear that it's worth it, or that it can be done without an ABI break. --- Changes.md | 1 + python/GafferUI/AnimationEditor.py | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Changes.md b/Changes.md index bd149aab715..1650bd6d098 100644 --- a/Changes.md +++ b/Changes.md @@ -19,6 +19,7 @@ Fixes - OpDialogue : Fixed `postExecuteBehaviour` handling. - LocalDispatcher : Fixed job status update when a job was killed _immediately_ after being launched. - `gaffer view` : Fixed default OpenColorIO display transform. +- AnimationEditor : Fixed changing of the current frame by dragging the frame indicator or clicking on the time axis. API --- diff --git a/python/GafferUI/AnimationEditor.py b/python/GafferUI/AnimationEditor.py index 5b77d3b234a..32173aafb0e 100644 --- a/python/GafferUI/AnimationEditor.py +++ b/python/GafferUI/AnimationEditor.py @@ -224,7 +224,14 @@ def _updateFromSet( self ) : def _updateFromContext( self, modifiedItems ) : - self.__animationGadget.setContext( self.context() ) + # Note that we're passing `scriptNode().context()` rather than + # `self.context()` because we don't want to use a ContextTracker-based + # context in the AnimationEditor. + ## \todo It would be better if `AnimationGadget::setContext()` connected + # to `Context::changedSignal()` and updated automatically after that. We + # could also consider removing `setContext()` entirely and connecting to + # the ScriptNode's context in the AnimationGadget constructor. + self.__animationGadget.setContext( self.scriptNode().context() ) def __updateGadgetSets( self, unused = None ) : From de6c1742740e35b7dcf54d1c24d2975415bab544 Mon Sep 17 00:00:00 2001 From: Paul-George Roberts Date: Mon, 11 Nov 2024 17:24:51 -0500 Subject: [PATCH 26/34] Visualisers : Contribute viewer visualisers --- contrib/visualisers/CsVisualiseOrientTool.cpp | 930 ++++++++++ contrib/visualisers/CsVisualiseOrientTool.h | 186 ++ contrib/visualisers/CsVisualiseValueTool.cpp | 1573 +++++++++++++++++ contrib/visualisers/CsVisualiseValueTool.h | 226 +++ contrib/visualisers/CsVisualiseVectorTool.cpp | 973 ++++++++++ contrib/visualisers/CsVisualiseVectorTool.h | 195 ++ .../visualisers/CsVisualiseVertexIdTool.cpp | 1373 ++++++++++++++ contrib/visualisers/CsVisualiseVertexIdTool.h | 223 +++ 8 files changed, 5679 insertions(+) create mode 100644 contrib/visualisers/CsVisualiseOrientTool.cpp create mode 100644 contrib/visualisers/CsVisualiseOrientTool.h create mode 100644 contrib/visualisers/CsVisualiseValueTool.cpp create mode 100644 contrib/visualisers/CsVisualiseValueTool.h create mode 100644 contrib/visualisers/CsVisualiseVectorTool.cpp create mode 100644 contrib/visualisers/CsVisualiseVectorTool.h create mode 100644 contrib/visualisers/CsVisualiseVertexIdTool.cpp create mode 100644 contrib/visualisers/CsVisualiseVertexIdTool.h diff --git a/contrib/visualisers/CsVisualiseOrientTool.cpp b/contrib/visualisers/CsVisualiseOrientTool.cpp new file mode 100644 index 00000000000..0f58f37b3c7 --- /dev/null +++ b/contrib/visualisers/CsVisualiseOrientTool.cpp @@ -0,0 +1,930 @@ +////////////////////////////////////////////////////////////////////////// +// +// 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +IECORE_PUSH_DEFAULT_VISIBILITY +#if GAFFER_COMPATIBILITY_VERSION < MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 3 ) +#include +#else +#include +#endif +IECORE_POP_DEFAULT_VISIBILITY + +#define private public +#include +#include +#undef private + +#include "CsVisualiseOrientTool.h" + +#include +#include +#include +#include +#include +#include +#if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) +#include +#else +#include +#endif + +#include +#include + +#include +#include +//#include +//#include +//#include +#include +#if ( GAFFER_MILESTONE_VERSION == 0 && GAFFER_MAJOR_VERSION < 61 ) +#include +#endif + +#if GAFFER_COMPATIBILITY_VERSION < MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 3 ) +#include +#include +#else +#include +#include +#endif + +#include +#include +#include + +#include +#include +#include +#include + +namespace +{ + // scale and colour constants + + float const g_scaleDefault = 1.f; + float const g_scaleMin = 10.f * std::numeric_limits< float >::min(); + float const g_scaleInc = 0.01f; + + Imath::Color3f const g_colourXDefault( 1.f, 0.f, 0.f ); + Imath::Color3f const g_colourYDefault( 0.f, 1.f, 0.f ); + Imath::Color3f const g_colourZDefault( 0.f, 0.f, 1.f ); + + // name of P primitive variable + + std::string const g_pName( "P" ); + + // uniform block structure (std140 layout) + + struct UniformBlock + { + alignas( 16 ) Imath::M44f o2c; + alignas( 16 ) Imath::Color3f colourX; + alignas( 16 ) Imath::Color3f colourY; + alignas( 16 ) Imath::Color3f colourZ; + alignas( 16 ) float scale; // NOTE : following vec3 array must align to 16 byte address + }; + + GLuint const g_uniformBlockBindingIndex = 0; + +# define UNIFORM_BLOCK_GLSL_SOURCE \ + "layout( std140, row_major ) uniform UniformBlock\n" \ + "{\n" \ + " mat4 o2c;\n" \ + " vec3 colour[ 3 ];\n" \ + " float scale;\n" \ + "} uniforms;\n" + +# define ATTRIB_GLSL_LOCATION_PS 0 +# define ATTRIB_GLSL_LOCATION_QS 1 + +# define ATTRIB_GLSL_SOURCE \ + "layout( location = " BOOST_PP_STRINGIZE( ATTRIB_GLSL_LOCATION_PS ) " ) in vec3 ps;\n" \ + "layout( location = " BOOST_PP_STRINGIZE( ATTRIB_GLSL_LOCATION_QS ) " ) in vec4 qs;\n" + +# define INTERFACE_BLOCK_GLSL_SOURCE( STORAGE, NAME ) \ + BOOST_PP_STRINGIZE( STORAGE ) " InterfaceBlock\n" \ + "{\n" \ + " flat vec3 colour;\n" \ + "} " BOOST_PP_STRINGIZE( NAME ) ";\n" + + // opengl vertex shader code + + std::string const g_vertSource + ( + "#version 330\n" + + UNIFORM_BLOCK_GLSL_SOURCE + + ATTRIB_GLSL_SOURCE + + INTERFACE_BLOCK_GLSL_SOURCE( out, outputs ) + + "void main()\n" + "{\n" + " vec3 position = ps;\n" + " int axis = gl_VertexID / 2;\n" + + " if( ( gl_VertexID % 2 ) > 0 )\n" + " {\n" + " float r = qs.x;\n" + " vec3 v = qs.yzw;\n" + " mat3 m = mat3(\n" + " 1.0 - 2.0 * dot( v.yz, v.yz ),\n" + " 2.0 * dot( v.xz, vec2( v.y, r ) ),\n" + " 2.0 * dot( v.zy, vec2( v.x, -r ) ),\n" + + " 2.0 * dot( v.xz, vec2( v.y, -r ) ),\n" + " 1.0 - 2.0 * dot( v.zx, v.zx ),\n" + " 2.0 * dot( v.yx, vec2( v.z, r ) ),\n" + + " 2.0 * dot( v.zy, vec2( v.x, r ) ),\n" + " 2.0 * dot( v.yx, vec2( v.z, -r ) ),\n" + " 1.0 - 2.0 * dot( v.yx, v.yx ) );\n" + + " position += normalize( m[ axis ] ) * uniforms.scale;\n" + " }\n" + + " gl_Position = vec4( position, 1.0 ) * uniforms.o2c;\n" + " outputs.colour = uniforms.colour[ axis ];\n" + "}\n" + ); + + // opengl fragment shader code + + std::string const g_fragSource + ( + "#version 330\n" + + UNIFORM_BLOCK_GLSL_SOURCE + + INTERFACE_BLOCK_GLSL_SOURCE( in, inputs ) + + "layout( location = 0 ) out vec4 cs;\n" + + "void main()\n" + "{\n" + " cs = vec4( inputs.colour, 1.0 );\n" + "}\n" + ); + + // the gadget that does the actual opengl drawing of the orientation frames + + struct Gadget + : public GafferUI::Gadget + { + explicit + Gadget + ( + CSGafferUI::CsVisualiseOrientTool const& tool, + std::string const& name = "CsVisualiseOrientGadget" + ) + : GafferUI::Gadget( name ) + , m_tool( & tool ) + , m_shader() + , m_uniformBuffer() + {} + + void resetTool() + { + m_tool = nullptr; + } + + protected: + + void +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 61 ) + renderLayer +# else + doRenderLayer +# endif + ( + GafferUI::Gadget::Layer layer, + GafferUI::Style const* style +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 61 ) + , GafferUI::Gadget::RenderReason reason +# endif + ) + const override + { + if( ( layer != GafferUI::Gadget::Layer::MidFront ) || +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 61 ) + ( GafferUI::Gadget::isSelectionRender( reason ) ) ) +# else + ( IECoreGL::Selector::currentSelector() ) ) +# endif + { + return; + } + + // check tool reference valid + + if( m_tool == nullptr ) + { + return; + } + + // get parent viewport gadget + + GafferUI::ViewportGadget const* const viewportGadget = + ancestor< GafferUI::ViewportGadget >(); + if( viewportGadget == nullptr ) + { + return; + } + + // bootleg shader + + buildShader(); + + if( ! m_shader ) + { + return; + } + + // get the cached converter from IECoreGL, this is used to convert primitive + // variable data to opengl buffers which will be shared with the IECoreGL renderer + + IECoreGL::CachedConverter* const converter = + IECoreGL::CachedConverter::defaultCachedConverter(); + + // bootleg uniform buffer + + GLint uniformBinding; + glGetIntegerv( GL_UNIFORM_BUFFER_BINDING, & uniformBinding ); + + if( ! m_uniformBuffer ) + { + GLuint buffer = 0u; + glGenBuffers( 1, & buffer ); + glBindBuffer( GL_UNIFORM_BUFFER, buffer ); + glBufferData( GL_UNIFORM_BUFFER, sizeof( UniformBlock ), 0, GL_DYNAMIC_DRAW ); + m_uniformBuffer.reset( new IECoreGL::Buffer( buffer ) ); + } + + glBindBufferBase( GL_UNIFORM_BUFFER, g_uniformBlockBindingIndex, m_uniformBuffer->m_buffer ); + + // get the name of the primitive variable to visualise + + std::string const& name = m_tool->namePlug()->getValue(); + + // get scale factor and colour + + UniformBlock uniforms; + uniforms.colourX = m_tool->colourXPlug()->getValue(); + uniforms.colourY = m_tool->colourYPlug()->getValue(); + uniforms.colourZ = m_tool->colourZPlug()->getValue(); + uniforms.scale = m_tool->scalePlug()->getValue(); + + // get the world to clip space matrix + + Imath::M44f v2c; + glGetFloatv( GL_PROJECTION_MATRIX, v2c.getValue() ); + Imath::M44f const w2c = viewportGadget->getCameraTransform().gjInverse() * v2c; + + // set opengl state + + GLfloat lineWidth; + glGetFloatv( GL_LINE_WIDTH, & lineWidth ); + glLineWidth( 1.f ); + + GLboolean const depthEnabled = glIsEnabled( GL_DEPTH_TEST ); + if( ! depthEnabled ) glEnable( GL_DEPTH_TEST ); + + GLboolean depthWriteEnabled; + glGetBooleanv( GL_DEPTH_WRITEMASK, & depthWriteEnabled ); + if( depthWriteEnabled ) glDepthMask( GL_FALSE ); + + GLboolean lineSmooth; + glGetBooleanv( GL_LINE_SMOOTH, & lineSmooth ); + if( lineSmooth ) glDisable( GL_LINE_SMOOTH ); + + GLboolean const blendEnabled = glIsEnabled( GL_BLEND ); + if( blendEnabled ) glDisable( GL_BLEND ); + + // enable shader program + + GLint shaderProgram; + glGetIntegerv( GL_CURRENT_PROGRAM, & shaderProgram ); + glUseProgram( m_shader->program() ); + + // set opengl vertex attribute array state + + GLint arrayBinding; + glGetIntegerv( GL_ARRAY_BUFFER_BINDING, & arrayBinding ); + + glPushClientAttrib( GL_CLIENT_VERTEX_ARRAY_BIT ); + + glVertexAttribDivisor( ATTRIB_GLSL_LOCATION_PS, 1 ); + glEnableVertexAttribArray( ATTRIB_GLSL_LOCATION_PS ); + glVertexAttribDivisor( ATTRIB_GLSL_LOCATION_QS, 1 ); + glEnableVertexAttribArray( ATTRIB_GLSL_LOCATION_QS ); + + // loop through current selection + + for( std::vector< CSGafferUI::CsVisualiseOrientTool::Selection >::const_iterator + it = m_tool->selection().begin(), + itEnd = m_tool->selection().end(); it != itEnd; ++it ) + { + GafferScene::ScenePlug::PathScope scope( &( ( *it ).context() ), &( ( *it ).path() ) ); + + // check path exists + + if( !( ( *it ).scene().existsPlug()->getValue() ) ) + { + continue; + } + + // extract primitive + + IECoreScene::ConstPrimitivePtr const primitive = + IECore::runTimeCast< IECoreScene::Primitive const >( + ( *it ).scene().objectPlug()->getValue() ); + + if( ! primitive ) + { + continue; + } + + // retrieve cached IECoreGL primitive + + IECoreGL::ConstPrimitivePtr const primitiveGL = + IECore::runTimeCast< IECoreGL::Primitive const >( + converter->convert( primitive.get() ) ); + + if( ! primitiveGL ) + { + continue; + } + + // find "P" vertex attribute + + IECoreGL::Primitive::AttributeMap::const_iterator const pit = + primitiveGL->m_vertexAttributes.find( g_pName ); + if( pit == primitiveGL->m_vertexAttributes.end() ) + { + continue; + } + + IECore::ConstV3fVectorDataPtr const pData = + IECore::runTimeCast< IECore::V3fVectorData const >( ( *pit ).second ); + if( ! pData ) + { + continue; + } + + // find named vertex attribute + // + // NOTE : conversion to IECoreGL mesh may generate vertex attributes (eg. "N") + // so check named primitive variable exists on IECore mesh primitive as well. + + IECoreGL::Primitive::AttributeMap::const_iterator const qit = + primitiveGL->m_vertexAttributes.find( name ); + if( ( qit == primitiveGL->m_vertexAttributes.end() ) || + ( primitive->variables.find( name ) == primitive->variables.end() ) ) + { + continue; + } + + IECore::ConstQuatfVectorDataPtr const qData = + IECore::runTimeCast< IECore::QuatfVectorData const >( ( *qit ).second ); + if( ! qData ) + { + continue; + } + + // retrieve cached opengl buffer data + + IECoreGL::ConstBufferPtr const pBuffer = + IECore::runTimeCast< IECoreGL::Buffer const >( converter->convert( pData.get() ) ); + IECoreGL::ConstBufferPtr const qBuffer = + IECore::runTimeCast< IECoreGL::Buffer const >( converter->convert( qData.get() ) ); + + // get the object to world transform + + Imath::M44f o2w; + GafferScene::ScenePlug::ScenePath path( ( *it ).path() ); + while( ! path.empty() ) + { + scope.setPath( & path ); + o2w = o2w * ( *it ).scene().transformPlug()->getValue(); + path.pop_back(); + } + + // compute object to clip matrix + + uniforms.o2c = o2w * w2c; + + // upload opengl uniform block data + + glBufferData( GL_UNIFORM_BUFFER, sizeof( UniformBlock ), & uniforms, GL_DYNAMIC_DRAW ); + + // instance a three line segments for each element of orientation data + + glBindBuffer( GL_ARRAY_BUFFER, pBuffer->m_buffer ); + glVertexAttribPointer( ATTRIB_GLSL_LOCATION_PS, 3, GL_FLOAT, GL_FALSE, 0, + static_cast< void const* >( 0 ) ); + glBindBuffer( GL_ARRAY_BUFFER, qBuffer->m_buffer ); + glVertexAttribPointer( ATTRIB_GLSL_LOCATION_QS, 4, GL_FLOAT, GL_FALSE, 0, + static_cast< void const* >( 0 ) ); + glDrawArraysInstanced( GL_LINES, 0, 6, static_cast< GLsizei >( pData->readable().size() ) ); + } + + // restore opengl state + + glPopClientAttrib(); + glBindBuffer( GL_ARRAY_BUFFER, arrayBinding ); + glBindBuffer( GL_UNIFORM_BUFFER, uniformBinding ); + + glLineWidth( lineWidth ); + + if( lineSmooth ) glEnable( GL_LINE_SMOOTH ); + if( blendEnabled ) glEnable( GL_BLEND ); + if( ! depthEnabled ) glDisable( GL_DEPTH_TEST ); + if( depthWriteEnabled ) glDepthMask( GL_TRUE ); + glUseProgram( shaderProgram ); + } + +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 61 ) + Imath::Box3f renderBound() const override + { + // NOTE : for now just return an infinite box + + Imath::Box3f b; + b.makeInfinite(); + return b; + } +# endif + +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 61 ) + unsigned layerMask() const override + { + return ( m_tool ) + ? static_cast< unsigned >( GafferUI::Gadget::Layer::MidFront ) + : static_cast< unsigned >( 0 ); + } +# else + bool hasLayer( GafferUI::Gadget::Layer layer ) + { + return ( m_tool && + ( layer == GafferUI::Gadget::Layer::MidFront ) ); + } +# endif + + private: + + void buildShader() const + { + if( ! m_shader ) + { + m_shader = IECoreGL::ShaderLoader::defaultShaderLoader()->create( + g_vertSource, std::string(), g_fragSource ); + if( m_shader ) + { + GLuint const program = m_shader->program(); + GLuint const blockIndex = glGetUniformBlockIndex( program, "UniformBlock" ); + if( blockIndex != GL_INVALID_INDEX ) + { + glUniformBlockBinding( program, blockIndex, g_uniformBlockBindingIndex ); + } + } + } + } + + CSGafferUI::CsVisualiseOrientTool const* m_tool; + mutable IECoreGL::ConstShaderPtr m_shader; + mutable IECoreGL::ConstBufferPtr m_uniformBuffer; + }; + +} // namespace + +namespace CSGafferUI +{ + GAFFER_NODE_DEFINE_TYPE( CsVisualiseOrientTool ) + + GafferUI::Tool::ToolDescription< CsVisualiseOrientTool, GafferSceneUI::SceneView > CsVisualiseOrientTool::m_toolDescription; + + size_t CsVisualiseOrientTool::m_firstPlugIndex = 0; + + CsVisualiseOrientTool::CsVisualiseOrientTool + ( + GafferSceneUI::SceneView* const view, + std::string const& name + ) + : GafferSceneUI::SelectionTool( view, name ) +# if GAFFER_COMPATIBILITY_VERSION < MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + , m_contextChangedConnection() +# endif + , m_preRenderConnection() + , m_gadget( new Gadget( *this ) ) + , m_selection() + , m_gadgetDirty( true ) + , m_selectionDirty( true ) + , m_priorityPathsDirty( true ) + { + // add gadget to view and hide + + view->viewportGadget()->addChild( m_gadget ); + m_gadget->setVisible( false ); + + // store offset of first plug + + storeIndexOfNextChild( m_firstPlugIndex ); + + // add child plugs + + addChild( new Gaffer::StringPlug( "name", Gaffer::Plug::In, "orient" ) ); + addChild( new Gaffer::FloatPlug( "scale", Gaffer::Plug::In, g_scaleDefault, g_scaleMin ) ); + addChild( new Gaffer::Color3fPlug( "colourX", Gaffer::Plug::In, g_colourXDefault ) ); + addChild( new Gaffer::Color3fPlug( "colourY", Gaffer::Plug::In, g_colourYDefault ) ); + addChild( new Gaffer::Color3fPlug( "colourZ", Gaffer::Plug::In, g_colourZDefault ) ); + addChild( new GafferScene::ScenePlug( "__scene", Gaffer::Plug::In ) ); + + // connect out internal scene plug to the parent view's scene plug + + internalScenePlug()->setInput( view->inPlug< GafferScene::ScenePlug >() ); + + // connect signal handlers + + view->viewportGadget()->keyPressSignal().connect( + boost::bind( & CsVisualiseOrientTool::keyPress, this, boost::placeholders::_2 ) ); + + plugDirtiedSignal().connect( + boost::bind( & CsVisualiseOrientTool::plugDirtied, this, boost::placeholders::_1 ) ); + +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + view->contextChangedSignal().connect( + boost::bind( & CsVisualiseOrientTool::contextChanged, this ) ); + GafferSceneUI::ScriptNodeAlgo::selectedPathsChangedSignal( view->scriptNode() ).connect( + boost::bind( &CsVisualiseOrientTool::selectedPathsChanged, this ) ); +# else + connectToViewContext(); + view->contextChangedSignal().connect( + boost::bind( & CsVisualiseOrientTool::connectToViewContext, this ) ); +# endif + + Gaffer::Metadata::plugValueChangedSignal().connect( + boost::bind( & CsVisualiseOrientTool::metadataChanged, this, boost::placeholders::_3 ) ); + Gaffer::Metadata::nodeValueChangedSignal().connect( + boost::bind( & CsVisualiseOrientTool::metadataChanged, this, boost::placeholders::_2 ) ); + } + + CsVisualiseOrientTool::~CsVisualiseOrientTool() + { + // NOTE : ensure that the gadget's reference to the tool is reset + + static_cast< Gadget* >( m_gadget.get() )->resetTool(); + } + + Gaffer::StringPlug* CsVisualiseOrientTool::namePlug() + { + return const_cast< Gaffer::StringPlug* >( + static_cast< CsVisualiseOrientTool const* >( this )->namePlug() ); + } + + Gaffer::StringPlug const* CsVisualiseOrientTool::namePlug() const + { + return getChild< Gaffer::StringPlug >( m_firstPlugIndex + 0 ); + } + + Gaffer::FloatPlug* CsVisualiseOrientTool::scalePlug() + { + return const_cast< Gaffer::FloatPlug* >( + static_cast< CsVisualiseOrientTool const* >( this )->scalePlug() ); + } + + Gaffer::FloatPlug const* CsVisualiseOrientTool::scalePlug() const + { + return getChild< Gaffer::FloatPlug >( m_firstPlugIndex + 1 ); + } + + Gaffer::Color3fPlug* CsVisualiseOrientTool::colourXPlug() + { + return const_cast< Gaffer::Color3fPlug* >( + static_cast< CsVisualiseOrientTool const* >( this )->colourXPlug() ); + } + + Gaffer::Color3fPlug const* CsVisualiseOrientTool::colourXPlug() const + { + return getChild< Gaffer::Color3fPlug >( m_firstPlugIndex + 2 ); + } + + Gaffer::Color3fPlug* CsVisualiseOrientTool::colourYPlug() + { + return const_cast< Gaffer::Color3fPlug* >( + static_cast< CsVisualiseOrientTool const* >( this )->colourYPlug() ); + } + + Gaffer::Color3fPlug const* CsVisualiseOrientTool::colourYPlug() const + { + return getChild< Gaffer::Color3fPlug >( m_firstPlugIndex + 3 ); + } + + Gaffer::Color3fPlug* CsVisualiseOrientTool::colourZPlug() + { + return const_cast< Gaffer::Color3fPlug* >( + static_cast< CsVisualiseOrientTool const* >( this )->colourZPlug() ); + } + + Gaffer::Color3fPlug const* CsVisualiseOrientTool::colourZPlug() const + { + return getChild< Gaffer::Color3fPlug >( m_firstPlugIndex + 4 ); + } + + GafferScene::ScenePlug* CsVisualiseOrientTool::internalScenePlug() + { + return const_cast< GafferScene::ScenePlug* >( + static_cast< CsVisualiseOrientTool const* >( this )->internalScenePlug() ); + } + + GafferScene::ScenePlug const* CsVisualiseOrientTool::internalScenePlug() const + { + return getChild< GafferScene::ScenePlug >( m_firstPlugIndex + 5 ); + } + + std::vector< CsVisualiseOrientTool::Selection > const& CsVisualiseOrientTool::selection() const + { + return m_selection; + } + +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + void CsVisualiseOrientTool::contextChanged() + { + // Context changes can change the scene, which in turn + // dirties our selection. + selectedPathsChanged(); + } + + void CsVisualiseOrientTool::selectedPathsChanged() + { + m_selectionDirty = true; + m_gadgetDirty = true; + m_priorityPathsDirty = true; + } +# else + void CsVisualiseOrientTool::connectToViewContext() + { + m_contextChangedConnection = view()->getContext()->changedSignal().connect( + boost::bind( & CsVisualiseOrientTool::contextChanged, this, boost::placeholders::_2 ) ); + } + + void CsVisualiseOrientTool::contextChanged + ( + IECore::InternedString const& name + ) + { + if( GafferSceneUI::ContextAlgo::affectsSelectedPaths( name ) || + GafferSceneUI::ContextAlgo::affectsLastSelectedPath( name ) || + ! boost::starts_with( name.string(), "ui:" ) ) + { + m_selectionDirty = true; + m_gadgetDirty = true; + m_priorityPathsDirty = true; + } + } +# endif + + void CsVisualiseOrientTool::plugDirtied + ( + Gaffer::Plug const* const plug + ) + { + if( ( plug == activePlug() ) || + ( plug == internalScenePlug()->objectPlug() ) || + ( plug == internalScenePlug()->transformPlug() ) ) + { + m_selectionDirty = true; + m_gadgetDirty = true; + m_priorityPathsDirty = true; + } + else + if( ( plug == namePlug() ) || + ( plug == scalePlug() ) || + ( plug == colourXPlug() ) || + ( plug == colourYPlug() ) || + ( plug == colourZPlug() ) ) + { + m_gadgetDirty = true; + view()->viewportGadget()->renderRequestSignal()( view()->viewportGadget() ); + } + + if( plug == activePlug() ) + { + if( activePlug()->getValue() ) + { + m_preRenderConnection = view()->viewportGadget()->preRenderSignal().connect( + boost::bind( & CsVisualiseOrientTool::preRender, this ) ); + } + else + { + m_preRenderConnection.disconnect(); + m_gadget->setVisible( false ); + + static_cast< GafferSceneUI::SceneGadget* >( view()->viewportGadget()->getPrimaryChild() ) + ->setPriorityPaths( IECore::PathMatcher() ); + } + } + } + + void CsVisualiseOrientTool::metadataChanged + ( + IECore::InternedString const& key + ) + { + if( ! Gaffer::MetadataAlgo::readOnlyAffectedByChange( key ) ) + { + return; + } + + if( ! m_selectionDirty ) + { + m_selectionDirty = true; + } + + if( ! m_gadgetDirty ) + { + m_gadgetDirty = true; + view()->viewportGadget()->renderRequestSignal()( view()->viewportGadget() ); + } + } + + void CsVisualiseOrientTool::updateSelection() const + { + if( ! m_selectionDirty ) + { + return; + } + + m_selection.clear(); + m_selectionDirty = false; + + if( ! activePlug()->getValue() ) + { + return; + } + + GafferScene::ScenePlug const* scene = + internalScenePlug()->getInput< GafferScene::ScenePlug >(); + + if( !( scene ) || + !( scene = scene->getInput< GafferScene::ScenePlug >() ) ) + { + return; + } + +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + IECore::PathMatcher const selectedPaths = + GafferSceneUI::ScriptNodeAlgo::getSelectedPaths( view()->scriptNode() ); +# else + IECore::PathMatcher const selectedPaths = + GafferSceneUI::ContextAlgo::getSelectedPaths( view()->getContext() ); +# endif + + if( selectedPaths.isEmpty() ) + { + return; + } + + for( IECore::PathMatcher::Iterator it = selectedPaths.begin(), + itEnd = selectedPaths.end(); it != itEnd; ++it ) + { +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + m_selection.emplace_back( *( scene ), *it, *( view()->context() ) ); +# else + m_selection.emplace_back( *( scene ), *it, *( view()->getContext() ) ); +# endif + } + } + + void CsVisualiseOrientTool::preRender() + { + updateSelection(); + + if( m_priorityPathsDirty ) + { +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + static_cast< GafferSceneUI::SceneGadget* >( view()->viewportGadget()->getPrimaryChild() ) + ->setPriorityPaths( ( m_selection.empty() ) + ? IECore::PathMatcher() + : GafferSceneUI::ScriptNodeAlgo::getSelectedPaths( view()->scriptNode() ) ); +# else + static_cast< GafferSceneUI::SceneGadget* >( view()->viewportGadget()->getPrimaryChild() ) + ->setPriorityPaths( ( m_selection.empty() ) + ? IECore::PathMatcher() + : GafferSceneUI::ContextAlgo::getSelectedPaths( view()->getContext() ) ); +# endif + m_priorityPathsDirty = false; + } + + if( m_selection.empty() ) + { + m_gadget->setVisible( false ); + return; + } + + m_gadget->setVisible( true ); + + if( m_gadgetDirty ) + { + m_gadgetDirty = false; + } + } + + bool CsVisualiseOrientTool::keyPress + ( + GafferUI::KeyEvent const& event + ) + { + if( ! activePlug()->getValue() ) + { + return false; + } + + // allow user to scale vectors with +/- keys + + if( event.key == "Plus" || event.key == "Equal" ) + { + scalePlug()->setValue( scalePlug()->getValue() + g_scaleInc ); + } + else + if( event.key == "Minus" || event.key == "Underscore" ) + { + scalePlug()->setValue( std::max( scalePlug()->getValue() - g_scaleInc, g_scaleMin ) ); + } + + return false; + } + + CsVisualiseOrientTool::Selection::Selection + ( + GafferScene::ScenePlug const& scene, + GafferScene::ScenePlug::ScenePath const& path, + Gaffer::Context const& context + ) + : m_scene( & scene ) + , m_path( path ) + , m_context( & context ) + {} + + GafferScene::ScenePlug const& CsVisualiseOrientTool::Selection::scene() const + { + return *( m_scene ); + } + + GafferScene::ScenePlug::ScenePath const& CsVisualiseOrientTool::Selection::path() const + { + return m_path; + } + + Gaffer::Context const& CsVisualiseOrientTool::Selection::context() const + { + return *( m_context ); + } + +} // CSGafferUI diff --git a/contrib/visualisers/CsVisualiseOrientTool.h b/contrib/visualisers/CsVisualiseOrientTool.h new file mode 100644 index 00000000000..5a14798a909 --- /dev/null +++ b/contrib/visualisers/CsVisualiseOrientTool.h @@ -0,0 +1,186 @@ +////////////////////////////////////////////////////////////////////////// +// +// 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. +// +////////////////////////////////////////////////////////////////////////// + +#ifndef CSGAFFERUI_TOOLS_CSVISUALISEORIENTTOOL_H +#define CSGAFFERUI_TOOLS_CSVISUALISEORIENTTOOL_H + +#include "../../GafferTypeIds.h" + +#include +#include +#include +#include +#include +#include + +namespace CSGafferUI +{ + /** + * @brief Tool that displays a named primitive variable of type Imath::Quatf. + */ + struct CsVisualiseOrientTool + : public GafferSceneUI::SelectionTool + { + /** + * @brief ctor + * @param view parent view + * @param name name for node + */ + explicit + CsVisualiseOrientTool + ( + GafferSceneUI::SceneView* view, + std::string const& name = Gaffer::GraphComponent::defaultName< CsVisualiseOrientTool >() + ); + + /** + * @brief dtor + */ + ~CsVisualiseOrientTool() override; + + /** + * @name GafferPlugAccessors + * @brief Gaffer plug accessor functions + * @{ + */ + + Gaffer::StringPlug* namePlug(); + Gaffer::StringPlug const* namePlug() const; + + Gaffer::FloatPlug* scalePlug(); + Gaffer::FloatPlug const* scalePlug() const; + + Gaffer::Color3fPlug* colourXPlug(); + Gaffer::Color3fPlug const* colourXPlug() const; + + Gaffer::Color3fPlug* colourYPlug(); + Gaffer::Color3fPlug const* colourYPlug() const; + + Gaffer::Color3fPlug* colourZPlug(); + Gaffer::Color3fPlug const* colourZPlug() const; + + /** + * @} + */ + + GAFFER_NODE_DECLARE_TYPE( + CSGafferUI::CsVisualiseOrientTool, + CSInternalTypes::CsVisualiseOrientToolTypeId, + GafferSceneUI::SelectionTool ); + + /** + * @brief Class encapsulating a selected scene location + */ + struct Selection + { + /** + * @brief ctor + * @param scene scene + * @param path scene path + * @param context context + */ + Selection + ( + GafferScene::ScenePlug const& scene, + GafferScene::ScenePlug::ScenePath const& path, + Gaffer::Context const& context + ); + + /** + * @brief Get the scene + * @return scene + */ + GafferScene::ScenePlug const& scene() const; + + /** + * @brief Get the scene path + * @return scene path + */ + GafferScene::ScenePlug::ScenePath const& path() const; + + /** + * @brief Get the context + * @return context + */ + Gaffer::Context const& context() const; + + private: + + GafferScene::ConstScenePlugPtr m_scene; + GafferScene::ScenePlug::ScenePath m_path; + Gaffer::ConstContextPtr m_context; + }; + + /** + * @brief Get the current selection + * @return current selection + */ + std::vector< Selection > const& selection() const; + + private: + + GafferScene::ScenePlug* internalScenePlug(); + GafferScene::ScenePlug const* internalScenePlug() const; + + void plugDirtied( Gaffer::Plug const* plug ); + void metadataChanged( IECore::InternedString const& key ); + void updateSelection() const; + void preRender(); + bool keyPress( GafferUI::KeyEvent const& event ); + +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + void contextChanged(); + void selectedPathsChanged(); +# else + void connectToViewContext(); + void contextChanged( IECore::InternedString const& name ); + Gaffer::Signals::ScopedConnection m_contextChangedConnection; +# endif + Gaffer::Signals::ScopedConnection m_preRenderConnection; + + GafferUI::GadgetPtr m_gadget; + mutable std::vector< Selection > m_selection; + bool m_gadgetDirty; + mutable bool m_selectionDirty; + bool m_priorityPathsDirty; + + static ToolDescription< CsVisualiseOrientTool, GafferSceneUI::SceneView > m_toolDescription; + static size_t m_firstPlugIndex; + }; + +} // CSGafferUI + +#endif // CSGAFFERUI_TOOLS_CSVISUALISEORIENTTOOL_H diff --git a/contrib/visualisers/CsVisualiseValueTool.cpp b/contrib/visualisers/CsVisualiseValueTool.cpp new file mode 100644 index 00000000000..5a2fdd02759 --- /dev/null +++ b/contrib/visualisers/CsVisualiseValueTool.cpp @@ -0,0 +1,1573 @@ +////////////////////////////////////////////////////////////////////////// +// +// 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +IECORE_PUSH_DEFAULT_VISIBILITY +#if GAFFER_COMPATIBILITY_VERSION < MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 3 ) +#include +#else +#include +#endif +IECORE_POP_DEFAULT_VISIBILITY + +#define private public +#include +#include +#undef private + +#include "CsVisualiseValueTool.h" + +#include +#include +#include +#include +#include +#include +#include +#if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) +#include +#else +#include +#endif + +#include +#include +#include +#include + +//#include +//#include +#include +#include +//#include +#include +#if ( GAFFER_MILESTONE_VERSION == 0 && GAFFER_MAJOR_VERSION < 61 ) +#include +#endif + +#if GAFFER_COMPATIBILITY_VERSION < MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 3 ) +#include +#else +#include +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace +{ + // text and size constants + + float const g_textSizeDefault = 9.0f; + float const g_textSizeMin = 6.0f; + float const g_textSizeInc = 0.5f; + + Imath::Color3f g_colourDefault( 1.f, 1.f, 1.f ); + + // opacity and value constants + + float const g_opacityDefault = 0.5f; + float const g_opacityMin = 0.0f; + float const g_opacityMax = 1.0f; + + Imath::V3f const g_valueMinDefault( 0.f ); + Imath::V3f const g_valueMaxDefault( 1.f ); + + // convert three component colour to four component colour with full opacity + + Imath::Color4f convertToColor4f + ( + Imath::Color3f const& c + ) + { + return Imath::Color4f( c[ 0 ], c[ 1 ], c[ 2 ], 1.f ); + } + + // name of P primitive variable + + std::string const g_pName( "P" ); + + // uniform block structure (std140 layout) + + struct UniformBlock + { + alignas( 16 ) Imath::M44f o2c; + alignas( 16 ) Imath::V3f valueMin; + alignas( 16 ) Imath::V3f valueRange; + alignas( 4 ) float opacity; + }; + + GLuint const g_uniformBlockBindingIndex = 0; + +# define UNIFORM_BLOCK_GLSL_SOURCE \ + "layout( std140, row_major ) uniform UniformBlock\n" \ + "{\n" \ + " mat4 o2c;\n" \ + " vec3 valueMin;\n" \ + " vec3 valueRange;\n" \ + " float opacity;\n" \ + "} uniforms;\n" + +# define ATTRIB_GLSL_LOCATION_PS 0 +# define ATTRIB_GLSL_LOCATION_VSX 1 +# define ATTRIB_GLSL_LOCATION_VSY 2 +# define ATTRIB_GLSL_LOCATION_VSZ 3 + +# define ATTRIB_GLSL_SOURCE \ + "layout( location = " BOOST_PP_STRINGIZE( ATTRIB_GLSL_LOCATION_PS ) " ) in vec3 ps;\n" \ + "layout( location = " BOOST_PP_STRINGIZE( ATTRIB_GLSL_LOCATION_VSX ) " ) in float vsx;\n" \ + "layout( location = " BOOST_PP_STRINGIZE( ATTRIB_GLSL_LOCATION_VSY ) " ) in float vsy;\n" \ + "layout( location = " BOOST_PP_STRINGIZE( ATTRIB_GLSL_LOCATION_VSZ ) " ) in float vsz;\n" \ + +# define INTERFACE_BLOCK_GLSL_SOURCE( STORAGE, NAME ) \ + BOOST_PP_STRINGIZE( STORAGE ) " InterfaceBlock\n" \ + "{\n" \ + " smooth vec3 value;\n" \ + "} " BOOST_PP_STRINGIZE( NAME ) ";\n" + + // opengl vertex shader code + + std::string const g_vertSource + ( + "#version 330\n" + + UNIFORM_BLOCK_GLSL_SOURCE + + ATTRIB_GLSL_SOURCE + + INTERFACE_BLOCK_GLSL_SOURCE( out, outputs ) + + "void main()\n" + "{\n" + " outputs.value = clamp( ( vec3( vsx, vsy, vsz ) - uniforms.valueMin )\n" + " * uniforms.valueRange, 0.0, 1.0 );\n" + " gl_Position = vec4( ps, 1.0 ) * uniforms.o2c;\n" + "}\n" + ); + + // opengl fragment shader code + + std::string const g_fragSource + ( + "#version 330\n" + + UNIFORM_BLOCK_GLSL_SOURCE + + INTERFACE_BLOCK_GLSL_SOURCE( in, inputs ) + + "layout( location = 0 ) out vec4 cs;\n" + + "void main()\n" + "{\n" + " cs = vec4( inputs.value, uniforms.opacity );\n" + "}\n" + ); + + // the gadget that does the actual opengl drawing of the shaded primitive + + struct Gadget + : public GafferUI::Gadget + { + explicit + Gadget + ( + CSGafferUI::CsVisualiseValueTool const& tool, + std::string const& name = "CsVisualiseValueGadget" + ) + : GafferUI::Gadget( name ) + , m_tool( & tool ) + , m_shader() + , m_uniformBuffer() + {} + + void resetTool() + { + m_tool = nullptr; + } + + protected: + + void +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 61 ) + renderLayer +# else + doRenderLayer +# endif + ( + GafferUI::Gadget::Layer layer, + GafferUI::Style const* style +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 61 ) + , GafferUI::Gadget::RenderReason reason +# endif + ) + const override + { + if( ( layer != GafferUI::Gadget::Layer::MidFront ) || +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 61 ) + ( GafferUI::Gadget::isSelectionRender( reason ) ) ) +# else + ( IECoreGL::Selector::currentSelector() ) ) +# endif + { + return; + } + + // check tool reference valid + + if( m_tool == nullptr ) + { + return; + } + + // get parent viewport gadget + + GafferUI::ViewportGadget const* const viewportGadget = + ancestor< GafferUI::ViewportGadget >(); + if( viewportGadget == nullptr ) + { + return; + } + + // bootleg shader + + buildShader(); + + if( ! m_shader ) + { + return; + } + + // get the cached converter from IECoreGL, this is used to convert primitive + // variable data to opengl buffers which will be shared with the IECoreGL renderer + + IECoreGL::CachedConverter* const converter = + IECoreGL::CachedConverter::defaultCachedConverter(); + + // bootleg uniform buffer + + GLint uniformBinding; + glGetIntegerv( GL_UNIFORM_BUFFER_BINDING, & uniformBinding ); + + if( ! m_uniformBuffer ) + { + GLuint buffer = 0u; + glGenBuffers( 1, & buffer ); + glBindBuffer( GL_UNIFORM_BUFFER, buffer ); + glBufferData( GL_UNIFORM_BUFFER, sizeof( UniformBlock ), 0, GL_DYNAMIC_DRAW ); + m_uniformBuffer.reset( new IECoreGL::Buffer( buffer ) ); + } + + glBindBufferBase( GL_UNIFORM_BUFFER, g_uniformBlockBindingIndex, m_uniformBuffer->m_buffer ); + + // get the name of the primitive variable to visualise + + std::string const& name = m_tool->namePlug()->getValue(); + + // get min/max values and colours and opacity + + UniformBlock uniforms; + Imath::V3f const valueMin = m_tool->valueMinPlug()->getValue(); + Imath::V3f const valueMax = m_tool->valueMaxPlug()->getValue(); + uniforms.opacity = m_tool->opacityPlug()->getValue(); + + // compute value range reciprocal + // + // NOTE : when range is <= 0 set the reciprocal to 0 so that value becomes 0 (minimum) + + Imath::V3f valueRange = ( valueMax - valueMin ); + for( int i = 0; i < 3; ++i ) + { + valueRange[ i ] = ( valueRange[ i ] > 0.f ) + ? ( 1.f / valueRange[ i ] ) : 0.f; + } + + // get the world to clip space matrix + + Imath::M44f v2c; + glGetFloatv( GL_PROJECTION_MATRIX, v2c.getValue() ); + Imath::M44f const w2c = viewportGadget->getCameraTransform().gjInverse() * v2c; + + // set opengl polygon and blend state + // + // NOTE : use polygon offset to ensure that any discrepancies between the transform + // from object to clip space do not cause z-fighting. This is necessary as + // the shader uses an object to clip matrix which may give slighly different + // depth results to the transformation used in the IECoreGL renderer. + + GLint blendEqRgb, blendEqAlpha; + glGetIntegerv( GL_BLEND_EQUATION_RGB, & blendEqRgb ); + glGetIntegerv( GL_BLEND_EQUATION_ALPHA, & blendEqAlpha ); + glBlendEquation( GL_FUNC_ADD ); + + GLint blendSrcRgb, blendSrcAlpha, blendDstRgb, blendDstAlpha; + glGetIntegerv( GL_BLEND_SRC_RGB, & blendSrcRgb ); + glGetIntegerv( GL_BLEND_SRC_ALPHA, & blendSrcAlpha ); + glGetIntegerv( GL_BLEND_DST_RGB, & blendDstRgb ); + glGetIntegerv( GL_BLEND_DST_ALPHA, & blendDstAlpha ); + glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA ); + + GLboolean const depthEnabled = glIsEnabled( GL_DEPTH_TEST ); + if( ! depthEnabled ) glEnable( GL_DEPTH_TEST ); + + GLint depthFunc; + glGetIntegerv( GL_DEPTH_FUNC, & depthFunc ); + glDepthFunc( GL_LEQUAL ); + + GLboolean depthWriteEnabled; + glGetBooleanv( GL_DEPTH_WRITEMASK, & depthWriteEnabled ); + if( depthWriteEnabled ) glDepthMask( GL_FALSE ); + + GLboolean const blendEnabled = glIsEnabled( GL_BLEND ); + if( ! blendEnabled ) glEnable( GL_BLEND ); + + GLint polygonMode; + glGetIntegerv( GL_POLYGON_MODE, & polygonMode ); + glPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); + + GLboolean const cullFaceEnabled = glIsEnabled( GL_CULL_FACE ); + if( cullFaceEnabled ) glDisable( GL_CULL_FACE ); + + GLboolean const polgonOffsetFillEnabled = glIsEnabled( GL_POLYGON_OFFSET_FILL ); + if( ! polgonOffsetFillEnabled ) glEnable( GL_POLYGON_OFFSET_FILL ); + + GLfloat polygonOffsetFactor, polygonOffsetUnits; + glGetFloatv( GL_POLYGON_OFFSET_FACTOR, & polygonOffsetFactor ); + glGetFloatv( GL_POLYGON_OFFSET_UNITS, & polygonOffsetUnits ); + glPolygonOffset( -1, -1 ); + + // enable shader program + + GLint shaderProgram; + glGetIntegerv( GL_CURRENT_PROGRAM, & shaderProgram ); + glUseProgram( m_shader->program() ); + + // set opengl vertex attribute array state + + GLint arrayBinding; + glGetIntegerv( GL_ARRAY_BUFFER_BINDING, & arrayBinding ); + + glPushClientAttrib( GL_CLIENT_VERTEX_ARRAY_BIT ); + + glVertexAttribDivisor( ATTRIB_GLSL_LOCATION_PS, 0 ); + glEnableVertexAttribArray( ATTRIB_GLSL_LOCATION_PS ); + glVertexAttribDivisor( ATTRIB_GLSL_LOCATION_VSX, 0 ); + glEnableVertexAttribArray( ATTRIB_GLSL_LOCATION_VSX ); + glVertexAttribDivisor( ATTRIB_GLSL_LOCATION_VSY, 0 ); + glEnableVertexAttribArray( ATTRIB_GLSL_LOCATION_VSY ); + glVertexAttribDivisor( ATTRIB_GLSL_LOCATION_VSZ, 0 ); + + // loop through current selection + + for( std::vector< CSGafferUI::CsVisualiseValueTool::Selection >::const_iterator + it = m_tool->selection().begin(), + itEnd = m_tool->selection().end(); it != itEnd; ++it ) + { + GafferScene::ScenePlug::PathScope scope( &( ( *it ).context() ), &( ( *it ).path() ) ); + + // check path exists + + if( !( ( *it ).scene().existsPlug()->getValue() ) ) + { + continue; + } + + // extract mesh primitive + + IECoreScene::ConstMeshPrimitivePtr const mesh = + IECore::runTimeCast< IECoreScene::MeshPrimitive const >( + ( *it ).scene().objectPlug()->getValue() ); + + if( ! mesh ) + { + continue; + } + + // retrieve cached IECoreGL mesh primitive + + IECoreGL::ConstPrimitivePtr const meshGL = + IECore::runTimeCast< IECoreGL::MeshPrimitive const >( + converter->convert( mesh.get() ) ); + + if( ! meshGL ) + { + continue; + } + + // find "P" vertex attribute + + IECoreGL::Primitive::AttributeMap::const_iterator const pit = + meshGL->m_vertexAttributes.find( g_pName ); + if( pit == meshGL->m_vertexAttributes.end() ) + { + continue; + } + + IECore::ConstV3fVectorDataPtr const pData = + IECore::runTimeCast< IECore::V3fVectorData const >( ( *pit ).second ); + if( ! pData ) + { + continue; + } + + // find named vertex attribute (FloatVectorData, V2fVectorData or V3fVectorData) + // + // NOTE : conversion to IECoreGL mesh may generate vertex attributes (eg. "N") + // so check named primitive variable exists on IECore mesh primitive. + + IECoreGL::Primitive::AttributeMap::const_iterator const vit = + meshGL->m_vertexAttributes.find( name ); + if( ( vit == meshGL->m_vertexAttributes.end() ) || !( ( *vit ).second ) || + ( mesh->variables.find( name ) == mesh->variables.end() ) ) + { + continue; + } + + IECore::ConstDataPtr const vData = ( *vit ).second; + GLsizei stride = 0; + GLenum type = GL_FLOAT; + bool offset = false; + bool enableVSZ = false; + switch( vData->typeId() ) + { + case IECore::IntVectorDataTypeId: + type = GL_INT; + case IECore::FloatVectorDataTypeId: + enableVSZ = true; + uniforms.valueMin = Imath::V3f( valueMin.x ); + uniforms.valueRange = Imath::V3f( valueRange.x ); + break; + case IECore::V2fVectorDataTypeId: + stride = 2; + offset = true; + uniforms.valueMin = Imath::V3f( valueMin.x, valueMin.y, 0.f ); + uniforms.valueRange = Imath::V3f( valueRange.x, valueRange.y, 0.f ); + break; + case IECore::V3fVectorDataTypeId: + stride = 3; + offset = true; + enableVSZ = true; + uniforms.valueMin = valueMin; + uniforms.valueRange = valueRange; + break; + default: + continue; + } + + // retrieve cached opengl buffer data + + IECoreGL::ConstBufferPtr const pBuffer = + IECore::runTimeCast< IECoreGL::Buffer const >( converter->convert( pData.get() ) ); + IECoreGL::ConstBufferPtr const vBuffer = + IECore::runTimeCast< IECoreGL::Buffer const >( converter->convert( vData.get() ) ); + + // get the object to world transform + + Imath::M44f o2w; + GafferScene::ScenePlug::ScenePath path( ( *it ).path() ); + while( ! path.empty() ) + { + scope.setPath( & path ); + o2w = o2w * ( *it ).scene().transformPlug()->getValue(); + path.pop_back(); + } + + // compute object to clip matrix + + uniforms.o2c = o2w * w2c; + + // upload opengl uniform block data + + glBufferData( GL_UNIFORM_BUFFER, sizeof( UniformBlock ), & uniforms, GL_DYNAMIC_DRAW ); + + // draw primitive + + glBindBuffer( GL_ARRAY_BUFFER, pBuffer->m_buffer ); + glVertexAttribPointer( ATTRIB_GLSL_LOCATION_PS, 3, GL_FLOAT, GL_FALSE, + 0, ( void const* )( 0 ) ); + glBindBuffer( GL_ARRAY_BUFFER, vBuffer->m_buffer ); + glVertexAttribPointer( ATTRIB_GLSL_LOCATION_VSX, 1, type, GL_FALSE, + stride * sizeof( GLfloat ), ( void const* )( 0 ) ); + glVertexAttribPointer( ATTRIB_GLSL_LOCATION_VSY, 1, type, GL_FALSE, + stride * sizeof( GLfloat ), ( void const* )( ( offset ? 1 : 0 ) * sizeof( GLfloat ) ) ); + if( enableVSZ ) + { + glEnableVertexAttribArray( ATTRIB_GLSL_LOCATION_VSZ ); + glVertexAttribPointer( ATTRIB_GLSL_LOCATION_VSZ, 1, type, GL_FALSE, + stride * sizeof( GLfloat ), ( void const* )( ( offset ? 2 : 0 ) * sizeof( GLfloat ) ) ); + } + else + { + glDisableVertexAttribArray( ATTRIB_GLSL_LOCATION_VSZ ); + glVertexAttrib1f( ATTRIB_GLSL_LOCATION_VSZ, 0.f ); + } + + meshGL->renderInstances( 1 ); + } + + // restore opengl state + + glPopClientAttrib(); + glBindBuffer( GL_ARRAY_BUFFER, arrayBinding ); + glBindBuffer( GL_UNIFORM_BUFFER, uniformBinding ); + + glDepthFunc( depthFunc ); + glBlendEquationSeparate( blendEqRgb, blendEqAlpha ); + glBlendFuncSeparate( blendSrcRgb, blendDstRgb, blendSrcAlpha, blendDstAlpha ); + glPolygonMode( GL_FRONT_AND_BACK, polygonMode ); + if( cullFaceEnabled ) glEnable( GL_CULL_FACE ); + if( ! polgonOffsetFillEnabled ) glDisable( GL_POLYGON_OFFSET_FILL ); + glPolygonOffset( polygonOffsetFactor, polygonOffsetUnits ); + + if( ! blendEnabled ) glDisable( GL_BLEND ); + if( ! depthEnabled ) glDisable( GL_DEPTH_TEST ); + if( depthWriteEnabled ) glDepthMask( GL_TRUE ); + glUseProgram( shaderProgram ); + + // display value at cursor as text + + IECore::Data const* const value = m_tool->cursorValue(); + if( value ) + { + std::ostringstream oss; + switch( value->typeId() ) + { + case IECore::IntDataTypeId: + oss << ( IECore::assertedStaticCast< IECore::IntData const >( value )->readable() ); + break; + case IECore::FloatDataTypeId: + oss << ( IECore::assertedStaticCast< IECore::FloatData const >( value )->readable() ); + break; + case IECore::V2fDataTypeId: + oss << ( IECore::assertedStaticCast< IECore::V2fData const >( value )->readable() ); + break; + case IECore::V3fDataTypeId: + oss << ( IECore::assertedStaticCast< IECore::V3fData const >( value )->readable() ); + break; + default: + break; + } + + std::string const text = oss.str(); + if( ! text.empty() ) + { + // draw in raster space + // + // NOTE : It seems that Gaffer defines the origin of raster space as the top left corner + // of the viewport, however the style text drawing functions assume that y increases + // "up" the screen rather than "down", so invert y to ensure text is not upside down. + + GafferUI::ViewportGadget::RasterScope raster( viewportGadget ); + float const size = m_tool->sizePlug()->getValue(); + Imath::V3f const scale( size, -size, 1.f ); + Imath::Color4f const colour = convertToColor4f( m_tool->colourPlug()->getValue() ); + Imath::V2f const& rp = m_tool->cursorPos(); + + glPushMatrix(); + glTranslatef( rp.x, rp.y, 0.f ); + glScalef( scale.x, scale.y, scale.z ); + style->renderText( GafferUI::Style::LabelText, text, GafferUI::Style::NormalState, & colour ); + glPopMatrix(); + } + } + } + +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 61 ) + Imath::Box3f renderBound() const override + { + // NOTE : for now just return an infinite box + + Imath::Box3f b; + b.makeInfinite(); + return b; + } +# endif + +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 61 ) + unsigned layerMask() const override + { + return ( m_tool ) + ? static_cast< unsigned >( GafferUI::Gadget::Layer::MidFront ) + : static_cast< unsigned >( 0 ); + } +# else + bool hasLayer( GafferUI::Gadget::Layer layer ) + { + return ( m_tool && + ( layer == GafferUI::Gadget::Layer::MidFront ) ); + } +# endif + + private: + + void buildShader() const + { + if( ! m_shader ) + { + m_shader = IECoreGL::ShaderLoader::defaultShaderLoader()->create( + g_vertSource, std::string(), g_fragSource ); + if( m_shader ) + { + GLuint const program = m_shader->program(); + GLuint const blockIndex = glGetUniformBlockIndex( program, "UniformBlock" ); + if( blockIndex != GL_INVALID_INDEX ) + { + glUniformBlockBinding( program, blockIndex, g_uniformBlockBindingIndex ); + } + } + } + } + + CSGafferUI::CsVisualiseValueTool const* m_tool; + mutable IECoreGL::ConstShaderPtr m_shader; + mutable IECoreGL::ConstBufferPtr m_uniformBuffer; + }; + + // cache for mesh evaluators + + struct EvaluationData + { + IECoreScene::ConstMeshPrimitivePtr triMesh; + IECoreScene::ConstMeshPrimitiveEvaluatorPtr evaluator; + }; + + IECore::LRUCache< + IECoreScene::ConstMeshPrimitivePtr, + EvaluationData > g_evaluatorCache( [] + ( + IECoreScene::ConstMeshPrimitivePtr const mesh, + size_t& cost + ) -> EvaluationData + { + cost = 1; + EvaluationData data; + data.triMesh = mesh->copy(); + data.triMesh = IECoreScene::MeshAlgo::triangulate( data.triMesh.get() ); + data.evaluator = new IECoreScene::MeshPrimitiveEvaluator( data.triMesh ); + return data; + }, 10 ); + +} // namespace + +namespace CSGafferUI +{ + GAFFER_NODE_DEFINE_TYPE( CsVisualiseValueTool ) + + GafferUI::Tool::ToolDescription< CsVisualiseValueTool, GafferSceneUI::SceneView > CsVisualiseValueTool::m_toolDescription; + + size_t CsVisualiseValueTool::m_firstPlugIndex = 0; + + CsVisualiseValueTool::CsVisualiseValueTool + ( + GafferSceneUI::SceneView* const view, + std::string const& name + ) + : GafferSceneUI::SelectionTool( view, name ) +# if GAFFER_COMPATIBILITY_VERSION < MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + , m_contextChangedConnection() +# endif + , m_preRenderConnection() + , m_buttonPressConnection() + , m_dragBeginConnection() + , m_gadget( new Gadget( *this ) ) + , m_selection() + , m_cursorPos( -1, -1 ) + , m_cursorPosValid( false ) + , m_cursorValue() + , m_gadgetDirty( true ) + , m_selectionDirty( true ) + , m_priorityPathsDirty( true ) + , m_acceptedButtonPress( false ) + , m_initiatedDrag( false ) + { + // add gadget to view and hide + + view->viewportGadget()->addChild( m_gadget ); + m_gadget->setVisible( false ); + + // store offset of first plug + + storeIndexOfNextChild( m_firstPlugIndex ); + + // add child plugs + + addChild( new Gaffer::StringPlug( "name", Gaffer::Plug::In, "uv" ) ); + addChild( new Gaffer::FloatPlug( "opacity", Gaffer::Plug::In, g_opacityDefault, g_opacityMin, g_opacityMax ) ); + addChild( new Gaffer::V3fPlug( "valueMin", Gaffer::Plug::In, g_valueMinDefault ) ); + addChild( new Gaffer::V3fPlug( "valueMax", Gaffer::Plug::In, g_valueMaxDefault ) ); + addChild( new Gaffer::FloatPlug( "size", Gaffer::Plug::In, g_textSizeDefault, g_textSizeMin ) ); + addChild( new Gaffer::Color3fPlug( "colour", Gaffer::Plug::In, g_colourDefault ) ); + addChild( new GafferScene::ScenePlug( "__scene", Gaffer::Plug::In ) ); + + // connect out internal scene plug to the parent view's scene plug + + internalScenePlug()->setInput( view->inPlug< GafferScene::ScenePlug >() ); + + // connect signal handlers + // + // NOTE : connecting to the viewport gadget means we will get called for all events + // which makes sense for key events, however we do not want to display value + // text when the mouse is over another gadget, (eg. Transform Tool handle) + // so instead connect to scene gadget signal. + // NOTE : There are other handlers that will attempt to consume button and drag + // events so connect handlers at the front of button/drag signal handler queues. + + view->viewportGadget()->keyPressSignal().connect( + boost::bind( & CsVisualiseValueTool::keyPress, this, boost::placeholders::_2 ) ); + + // NOTE : drag end and button release handlers remain whilst tool inactive in case tool + // is made inactive after button pressed or drag initiated in which case these + // handlers still need to tidy up state. + + sceneGadget()->buttonReleaseSignal(). +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 62 ) + connectFront +# else + connect +# endif + ( boost::bind( & CsVisualiseValueTool::buttonRelease, this, boost::placeholders::_2 ) +# if ( GAFFER_MILESTONE_VERSION == 0 && GAFFER_MAJOR_VERSION < 62 ) + , boost::signals::at_front +# endif + ); + + sceneGadget()->dragEndSignal(). +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 62 ) + connectFront +# else + connect +# endif + ( boost::bind( & CsVisualiseValueTool::dragEnd, this, boost::placeholders::_2 ) +# if ( GAFFER_MILESTONE_VERSION == 0 && GAFFER_MAJOR_VERSION < 62 ) + , boost::signals::at_front +# endif + ); + + // NOTE : mouse tracking handlers remain connected whilst tool inactive as they track the cursor + // line and whether its valid or not. This prevents the value display from "sticking" to + // edge of viewport when cursor leaves viewport's screen space. It also means that we do + // not have to work out the cursor line and whether its valid when tool is made active. + + sceneGadget()->enterSignal().connect( + boost::bind( & CsVisualiseValueTool::enter, this, boost::placeholders::_2 ) ); + sceneGadget()->leaveSignal().connect( + boost::bind( & CsVisualiseValueTool::leave, this, boost::placeholders::_2 ) ); + sceneGadget()->mouseMoveSignal().connect( + boost::bind( & CsVisualiseValueTool::mouseMove, this, boost::placeholders::_2 ) ); + + plugDirtiedSignal().connect( + boost::bind( & CsVisualiseValueTool::plugDirtied, this, boost::placeholders::_1 ) ); + plugSetSignal().connect( + boost::bind( & CsVisualiseValueTool::plugSet, this, boost::placeholders::_1 ) ); + +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + view->contextChangedSignal().connect( + boost::bind( & CsVisualiseValueTool::contextChanged, this ) ); + GafferSceneUI::ScriptNodeAlgo::selectedPathsChangedSignal( view->scriptNode() ).connect( + boost::bind( &CsVisualiseValueTool::selectedPathsChanged, this ) ); +# else + connectToViewContext(); + view->contextChangedSignal().connect( + boost::bind( & CsVisualiseValueTool::connectToViewContext, this ) ); +# endif + + Gaffer::Metadata::plugValueChangedSignal().connect( + boost::bind( & CsVisualiseValueTool::metadataChanged, this, boost::placeholders::_3 ) ); + Gaffer::Metadata::nodeValueChangedSignal().connect( + boost::bind( & CsVisualiseValueTool::metadataChanged, this, boost::placeholders::_2 ) ); + } + + CsVisualiseValueTool::~CsVisualiseValueTool() + { + // NOTE : ensure that the gadget's reference to the tool is reset + + static_cast< Gadget* >( m_gadget.get() )->resetTool(); + } + + Gaffer::StringPlug* CsVisualiseValueTool::namePlug() + { + return const_cast< Gaffer::StringPlug* >( + static_cast< CsVisualiseValueTool const* >( this )->namePlug() ); + } + + Gaffer::StringPlug const* CsVisualiseValueTool::namePlug() const + { + return getChild< Gaffer::StringPlug >( m_firstPlugIndex + 0 ); + } + + Gaffer::FloatPlug* CsVisualiseValueTool::opacityPlug() + { + return const_cast< Gaffer::FloatPlug* >( + static_cast< CsVisualiseValueTool const* >( this )->opacityPlug() ); + } + + Gaffer::FloatPlug const* CsVisualiseValueTool::opacityPlug() const + { + return getChild< Gaffer::FloatPlug >( m_firstPlugIndex + 1 ); + } + + Gaffer::V3fPlug* CsVisualiseValueTool::valueMinPlug() + { + return const_cast< Gaffer::V3fPlug* >( + static_cast< CsVisualiseValueTool const* >( this )->valueMinPlug() ); + } + + Gaffer::V3fPlug const* CsVisualiseValueTool::valueMinPlug() const + { + return getChild< Gaffer::V3fPlug >( m_firstPlugIndex + 2 ); + } + + Gaffer::V3fPlug* CsVisualiseValueTool::valueMaxPlug() + { + return const_cast< Gaffer::V3fPlug* >( + static_cast< CsVisualiseValueTool const* >( this )->valueMaxPlug() ); + } + + Gaffer::V3fPlug const* CsVisualiseValueTool::valueMaxPlug() const + { + return getChild< Gaffer::V3fPlug >( m_firstPlugIndex + 3 ); + } + + Gaffer::FloatPlug* CsVisualiseValueTool::sizePlug() + { + return const_cast< Gaffer::FloatPlug* >( + static_cast< CsVisualiseValueTool const* >( this )->sizePlug() ); + } + + Gaffer::FloatPlug const* CsVisualiseValueTool::sizePlug() const + { + return getChild< Gaffer::FloatPlug >( m_firstPlugIndex + 4 ); + } + + Gaffer::Color3fPlug* CsVisualiseValueTool::colourPlug() + { + return const_cast< Gaffer::Color3fPlug* >( + static_cast< CsVisualiseValueTool const* >( this )->colourPlug() ); + } + + Gaffer::Color3fPlug const* CsVisualiseValueTool::colourPlug() const + { + return getChild< Gaffer::Color3fPlug >( m_firstPlugIndex + 5 ); + } + + GafferScene::ScenePlug* CsVisualiseValueTool::internalScenePlug() + { + return const_cast< GafferScene::ScenePlug* >( + static_cast< CsVisualiseValueTool const* >( this )->internalScenePlug() ); + } + + GafferScene::ScenePlug const* CsVisualiseValueTool::internalScenePlug() const + { + return getChild< GafferScene::ScenePlug >( m_firstPlugIndex + 6 ); + } + + std::vector< CsVisualiseValueTool::Selection > const& CsVisualiseValueTool::selection() const + { + return m_selection; + } + + Imath::V2f CsVisualiseValueTool::cursorPos() const + { + return m_cursorPos; + } + + IECore::Data const* CsVisualiseValueTool::cursorValue() const + { + return m_cursorValue.get(); + } + + void CsVisualiseValueTool::connectOnActive() + { + // NOTE : There are other handlers that will attempt to consume button and drag events + // so connect handlers at the front of button/drag signal handler queues. + + m_buttonPressConnection = sceneGadget()->buttonPressSignal(). +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 62 ) + connectFront +# else + connect +# endif + ( boost::bind( & CsVisualiseValueTool::buttonPress, this, boost::placeholders::_2 ) +# if ( GAFFER_MILESTONE_VERSION == 0 && GAFFER_MAJOR_VERSION < 62 ) + , boost::signals::at_front +# endif + ); + m_dragBeginConnection = sceneGadget()->dragBeginSignal(). +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 62 ) + connectFront +# else + connect +# endif + ( boost::bind( & CsVisualiseValueTool::dragBegin, this, boost::placeholders::_2 ) +# if ( GAFFER_MILESTONE_VERSION == 0 && GAFFER_MAJOR_VERSION < 62 ) + , boost::signals::at_front +# endif + ); + + m_preRenderConnection = view()->viewportGadget()->preRenderSignal().connect( + boost::bind( & CsVisualiseValueTool::preRender, this ) ); + + // NOTE : redraw necessary to ensure value display updated. + + view()->viewportGadget()->renderRequestSignal()( view()->viewportGadget() ); + } + + void CsVisualiseValueTool::disconnectOnInactive() + { + m_preRenderConnection.disconnect(); + m_buttonPressConnection.disconnect(); + m_dragBeginConnection.disconnect(); + } + +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + void CsVisualiseValueTool::contextChanged() + { + // Context changes can change the scene, which in turn + // dirties our selection. + selectedPathsChanged(); + } + + void CsVisualiseValueTool::selectedPathsChanged() + { + m_selectionDirty = true; + m_gadgetDirty = true; + m_priorityPathsDirty = true; + } +# else + void CsVisualiseValueTool::connectToViewContext() + { + m_contextChangedConnection = view()->getContext()->changedSignal().connect( + boost::bind( & CsVisualiseValueTool::contextChanged, this, boost::placeholders::_2 ) ); + } + + void CsVisualiseValueTool::contextChanged + ( + IECore::InternedString const& name + ) + { + if( GafferSceneUI::ContextAlgo::affectsSelectedPaths( name ) || + GafferSceneUI::ContextAlgo::affectsLastSelectedPath( name ) || + ! boost::starts_with( name.string(), "ui:" ) ) + { + m_selectionDirty = true; + m_gadgetDirty = true; + m_priorityPathsDirty = true; + } + } +# endif + + bool CsVisualiseValueTool::mouseMove + ( + GafferUI::ButtonEvent const& event + ) + { + if( m_initiatedDrag ) + { + return false; + } + + updateCursorPos( event ); + m_cursorPosValid = true; + + // NOTE : only schedule redraw if tool active + + if( activePlug()->getValue() ) + { + view()->viewportGadget()->renderRequestSignal()( view()->viewportGadget() ); + } + + return false; + } + + void CsVisualiseValueTool::enter + ( + GafferUI::ButtonEvent const& event + ) + { + updateCursorPos( event ); + m_cursorPosValid = true; + + // NOTE : only schedule redraw if tool active + + if( activePlug()->getValue() ) + { + view()->viewportGadget()->renderRequestSignal()( view()->viewportGadget() ); + } + } + + void CsVisualiseValueTool::leave + ( + GafferUI::ButtonEvent const& event + ) + { + updateCursorPos( event ); + m_cursorPosValid = false; + + // NOTE : only schedule redraw if tool active + + if( activePlug()->getValue() ) + { + view()->viewportGadget()->renderRequestSignal()( view()->viewportGadget() ); + } + } + + bool CsVisualiseValueTool::keyPress + ( + GafferUI::KeyEvent const& event + ) + { + if( ! activePlug()->getValue() ) + { + return false; + } + + // allow user to scale text with +/- keys + + if( event.key == "Plus" || event.key == "Equal" ) + { + sizePlug()->setValue( sizePlug()->getValue() + g_textSizeInc ); + } + else + if( event.key == "Minus" || event.key == "Underscore" ) + { + sizePlug()->setValue( std::max( sizePlug()->getValue() - g_textSizeInc, g_textSizeMin ) ); + } + + return false; + } + + bool CsVisualiseValueTool::buttonPress + ( + GafferUI::ButtonEvent const& event + ) + { + m_acceptedButtonPress = false; + m_initiatedDrag = false; + + if( ( event.button & GafferUI::ButtonEvent::Left ) ) + { + updateCursorValue(); + if( m_cursorValue ) + { + m_acceptedButtonPress = true; + return true; + } + } + + return false; + } + + bool CsVisualiseValueTool::buttonRelease + ( + GafferUI::ButtonEvent const& event + ) + { + m_acceptedButtonPress = false; + m_initiatedDrag = false; + + return false; + } + + IECore::RunTimeTypedPtr + CsVisualiseValueTool::dragBegin + ( + GafferUI::DragDropEvent const& event + ) + { + m_initiatedDrag = false; + + if( ! m_acceptedButtonPress ) + { + return IECore::RunTimeTypedPtr(); + } + + m_acceptedButtonPress = false; + + if( m_cursorValue ) + { + // NOTE : There is a possibility that the tool has become inactive since the button + // press event that triggered the drag was accepted, the cutoff point is the + // button press event, so any change to the active state after that does not + // affect an ongoing drag operation. We therefore always request a redraw + // here so that the displayed value is cleared. + + m_initiatedDrag = true; + view()->viewportGadget()->renderRequestSignal()( view()->viewportGadget() ); + GafferUI::Pointer::setCurrent( "values" ); + } + + return m_cursorValue; + } + + bool CsVisualiseValueTool::dragEnd + ( + GafferUI::DragDropEvent const& event + ) + { + if( ! m_initiatedDrag ) + { + return false; + } + + m_initiatedDrag = false; + GafferUI::Pointer::setCurrent( "" ); + return true; + } + + void CsVisualiseValueTool::plugDirtied + ( + Gaffer::Plug const* const plug + ) + { + if( ( plug == activePlug() ) || + ( plug == internalScenePlug()->objectPlug() ) || + ( plug == internalScenePlug()->transformPlug() ) ) + { + m_selectionDirty = true; + m_gadgetDirty = true; + m_priorityPathsDirty = true; + } + else + if( ( plug == namePlug() ) || + ( plug == opacityPlug() ) || + ( plug == valueMinPlug() ) || + ( plug == valueMaxPlug() ) || + ( plug == sizePlug() ) || + ( plug == colourPlug() ) ) + { + m_gadgetDirty = true; + view()->viewportGadget()->renderRequestSignal()( view()->viewportGadget() ); + } + + if( plug == activePlug() ) + { + if( activePlug()->getValue() ) + { + connectOnActive(); + } + else + { + disconnectOnInactive(); + m_gadget->setVisible( false ); + + sceneGadget()->setPriorityPaths( IECore::PathMatcher() ); + } + } + } + + void CsVisualiseValueTool::plugSet( Gaffer::Plug* const plug ) + { + // ensure that the min value does not exceed the max and vice-versa + + if( plug == valueMinPlug() ) + { + Imath::V3f const valueMin = valueMinPlug()->getValue(); + Imath::V3f valueMax = valueMaxPlug()->getValue(); + + for( int i = 0; i < 3; ++i ) + { + valueMax[ i ] = std::max( valueMin[ i ], valueMax[ i ] ); + } + + valueMaxPlug()->setValue( valueMax ); + } + else + if( plug == valueMaxPlug() ) + { + Imath::V3f valueMin = valueMinPlug()->getValue(); + Imath::V3f const valueMax = valueMaxPlug()->getValue(); + + for( int i = 0; i < 3; ++i ) + { + valueMin[ i ] = std::min( valueMin[ i ], valueMax[ i ] ); + } + + valueMinPlug()->setValue( valueMin ); + } + } + + void CsVisualiseValueTool::metadataChanged + ( + IECore::InternedString const& key + ) + { + if( ! Gaffer::MetadataAlgo::readOnlyAffectedByChange( key ) ) + { + return; + } + + if( ! m_selectionDirty ) + { + m_selectionDirty = true; + } + + if( ! m_gadgetDirty ) + { + m_gadgetDirty = true; + view()->viewportGadget()->renderRequestSignal()( view()->viewportGadget() ); + } + } + + void CsVisualiseValueTool::updateSelection() const + { + if( ! m_selectionDirty ) + { + return; + } + + m_selection.clear(); + m_selectionDirty = false; + + if( ! activePlug()->getValue() ) + { + return; + } + + GafferScene::ScenePlug const* scene = + internalScenePlug()->getInput< GafferScene::ScenePlug >(); + + if( !( scene ) || + !( scene = scene->getInput< GafferScene::ScenePlug >() ) ) + { + return; + } + +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + IECore::PathMatcher const selectedPaths = + GafferSceneUI::ScriptNodeAlgo::getSelectedPaths( view()->scriptNode() ); +# else + IECore::PathMatcher const selectedPaths = + GafferSceneUI::ContextAlgo::getSelectedPaths( view()->getContext() ); +# endif + + if( selectedPaths.isEmpty() ) + { + return; + } + + for( IECore::PathMatcher::Iterator it = selectedPaths.begin(), + itEnd = selectedPaths.end(); it != itEnd; ++it ) + { +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + m_selection.emplace_back( *( scene ), *it, *( view()->context() ) ); +# else + m_selection.emplace_back( *( scene ), *it, *( view()->getContext() ) ); +# endif + + } + } + + void CsVisualiseValueTool::preRender() + { + updateSelection(); + + if( m_priorityPathsDirty ) + { +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + sceneGadget()->setPriorityPaths( ( m_selection.empty() ) + ? IECore::PathMatcher() + : GafferSceneUI::ScriptNodeAlgo::getSelectedPaths( view()->scriptNode() ) ); +# else + sceneGadget()->setPriorityPaths( ( m_selection.empty() ) + ? IECore::PathMatcher() + : GafferSceneUI::ContextAlgo::getSelectedPaths( view()->getContext() ) ); +# endif + + m_priorityPathsDirty = false; + } + + if( m_selection.empty() ) + { + m_gadget->setVisible( false ); + return; + } + + m_gadget->setVisible( true ); + + if( m_gadgetDirty ) + { + m_gadgetDirty = false; + } + + updateCursorValue(); + } + + void CsVisualiseValueTool::updateCursorPos + ( + GafferUI::ButtonEvent const& event + ) + { + // update cursor raster position + // + // NOTE : the cursor position is stored in raster space so it is free of camera + // transformations so we do not need to track camera changes. + + assert( view() ); + assert( view()->viewportGadget() ); + + m_cursorPos = view()->viewportGadget()->gadgetToRasterSpace( event.line.p1, sceneGadget() ); + } + + void CsVisualiseValueTool::updateCursorValue() + { + IECore::DataPtr cursorValue = m_cursorValue; + m_cursorValue.reset(); + + // NOTE : during a drag do not update the cursor value + + if( m_initiatedDrag || ! m_cursorPosValid ) + { + return; + } + + // get scene gadget and viewport gadgets + + GafferSceneUI::SceneGadget* const sg = sceneGadget(); + if( ! sg || ! view() || !( view()->viewportGadget() ) ) + { + return; + } + + // clear any existing selection mask + + IECore::StringVectorData const* const selectionMask = sg->getSelectionMask(); + sg->setSelectionMask( nullptr ); + + // get the current object at cursor + + GafferScene::ScenePlug::ScenePath path; + + try + { + if( ! sg->objectAt( view()->viewportGadget()->rasterToGadgetSpace( m_cursorPos, sg ), path ) ) + { + return; + } + } + catch( IECore::Exception const& e ) + { + // NOTE : objectAt seems to write to the OpenGL color buffer so if there was an + // error the OpenGL color buffer will contain the remnants of the failed + // object id pass. If we are being called from preRender() the color buffer + // would normally be cleared after the preRender callback has finished so + // catch the exception and return. If we are being called from button press + // we don't want the exception to propagate so again catch and return. In + // both cases the error should happen again during the next render pass. + + return; + } + + // check current object is included in selection + + std::vector< Selection >::const_iterator const sit = + std::find_if( m_selection.begin(), m_selection.end(), + [ & path ]( Selection const& item ) -> bool + { + return item.path() == path; + } ); + if( sit == m_selection.end() ) + { + return; + } + + // check scene location exists + + Selection const& item = ( *sit ); + GafferScene::ScenePlug::PathScope scope( &( item.context() ), & path ); + if( !( item.scene().existsPlug()->getValue() ) ) + { + return; + } + + // extract mesh primitive object + + IECoreScene::ConstMeshPrimitivePtr const mesh = + IECore::runTimeCast< IECoreScene::MeshPrimitive const >( + item.scene().objectPlug()->getValue() ); + if( ! mesh ) + { + return; + } + + // check mesh has named primitive variable + + std::string const& name = namePlug()->getValue(); + IECoreScene::PrimitiveVariableMap::const_iterator const vit = mesh->variables.find( name ); + if( vit == mesh->variables.end() || !( ( *vit ).second.data ) ) + { + return; + } + + // check type of data + + switch( ( *vit ).second.data->typeId() ) + { + case IECore::IntVectorDataTypeId: + case IECore::FloatVectorDataTypeId: + case IECore::V2fVectorDataTypeId: + case IECore::V3fVectorDataTypeId: + break; + default: + return; + } + + // create a mesh primitive evaluator + // + // NOTE : In order to create an evaluator we need a triangulated mesh + // this processing is expensive so we cache the created evaluator in an LRU cache + + EvaluationData const evalData = g_evaluatorCache.get( mesh ); + IECoreScene::PrimitiveEvaluator::ResultPtr const result = evalData.evaluator->createResult(); + + // intersect line from cursor with mesh in object space using evaluator + + IECore::LineSegment3f const line = + view()->viewportGadget()->rasterToWorldSpace( cursorPos() ) * + item.scene().fullTransform( path ).gjInverse(); + if( ! evalData.evaluator->intersectionPoint( line.p0, line.direction(), result.get() ) ) + { + return; + } + + // update value from intersection result + + switch( ( *vit ).second.data->typeId() ) + { + case IECore::IntVectorDataTypeId: + { + IECore::IntDataPtr data = + IECore::runTimeCast< IECore::IntData >( cursorValue ); + if( ! data ) data.reset( new IECore::IntData() ); + data->writable() = result->intPrimVar( evalData.triMesh->variables.at( name ) ); + cursorValue = data; + break; + } + case IECore::FloatVectorDataTypeId: + { + IECore::FloatDataPtr data = + IECore::runTimeCast< IECore::FloatData >( cursorValue ); + if( ! data ) data.reset( new IECore::FloatData() ); + data->writable() = result->floatPrimVar( evalData.triMesh->variables.at( name ) ); + cursorValue = data; + break; + } + case IECore::V2fVectorDataTypeId: + { + IECore::V2fDataPtr data = + IECore::runTimeCast< IECore::V2fData >( cursorValue ); + if( ! data ) data.reset( new IECore::V2fData() ); + data->writable() = result->vec2PrimVar( evalData.triMesh->variables.at( name ) ); + cursorValue = data; + break; + } + case IECore::V3fVectorDataTypeId: + { + IECore::V3fDataPtr data = + IECore::runTimeCast< IECore::V3fData >( cursorValue ); + if( ! data ) data.reset( new IECore::V3fData() ); + data->writable() = result->vectorPrimVar( evalData.triMesh->variables.at( name ) ); + cursorValue = data; + break; + } + default: + return; + } + + m_cursorValue = cursorValue; + + // restore selection mask + + sg->setSelectionMask( selectionMask ); + } + + GafferSceneUI::SceneGadget* CsVisualiseValueTool::sceneGadget() + { + return const_cast< GafferSceneUI::SceneGadget* >( + static_cast< CsVisualiseValueTool const* >( this )->sceneGadget() ); + } + + GafferSceneUI::SceneGadget const* CsVisualiseValueTool::sceneGadget() const + { + return IECore::runTimeCast< GafferSceneUI::SceneGadget const >( + view()->viewportGadget()->getPrimaryChild() ); + } + + CsVisualiseValueTool::Selection::Selection + ( + GafferScene::ScenePlug const& scene, + GafferScene::ScenePlug::ScenePath const& path, + Gaffer::Context const& context + ) + : m_scene( & scene ) + , m_path( path ) + , m_context( & context ) + {} + + GafferScene::ScenePlug const& CsVisualiseValueTool::Selection::scene() const + { + return *( m_scene ); + } + + GafferScene::ScenePlug::ScenePath const& CsVisualiseValueTool::Selection::path() const + { + return m_path; + } + + Gaffer::Context const& CsVisualiseValueTool::Selection::context() const + { + return *( m_context ); + } + +} // CSGafferUI diff --git a/contrib/visualisers/CsVisualiseValueTool.h b/contrib/visualisers/CsVisualiseValueTool.h new file mode 100644 index 00000000000..7dbbdd832a2 --- /dev/null +++ b/contrib/visualisers/CsVisualiseValueTool.h @@ -0,0 +1,226 @@ +////////////////////////////////////////////////////////////////////////// +// +// 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. +// +////////////////////////////////////////////////////////////////////////// + +#ifndef CSGAFFERUI_TOOLS_CSVISUALISEVALUETOOL_H +#define CSGAFFERUI_TOOLS_CSVISUALISEVALUETOOL_H + +#include "../../GafferTypeIds.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace CSGafferUI +{ + /** + * @brief Tool that displays a named primitive variable of type float/V2f/V3f + * of a MeshPrimitive as a coloured overlay. + */ + struct CsVisualiseValueTool + : public GafferSceneUI::SelectionTool + { + /** + * @brief ctor + * @param view parent view + * @param name name for node + */ + explicit + CsVisualiseValueTool + ( + GafferSceneUI::SceneView* view, + std::string const& name = Gaffer::GraphComponent::defaultName< CsVisualiseValueTool >() + ); + + /** + * @brief dtor + */ + ~CsVisualiseValueTool() override; + + /** + * @name GafferPlugAccessors + * @brief Gaffer plug accessor functions + * @{ + */ + + Gaffer::StringPlug* namePlug(); + Gaffer::StringPlug const* namePlug() const; + + Gaffer::FloatPlug* opacityPlug(); + Gaffer::FloatPlug const* opacityPlug() const; + + Gaffer::V3fPlug* valueMinPlug(); + Gaffer::V3fPlug const* valueMinPlug() const; + + Gaffer::V3fPlug* valueMaxPlug(); + Gaffer::V3fPlug const* valueMaxPlug() const; + + Gaffer::FloatPlug* sizePlug(); + Gaffer::FloatPlug const* sizePlug() const; + + Gaffer::Color3fPlug* colourPlug(); + Gaffer::Color3fPlug const* colourPlug() const; + + /** + * @} + */ + + GAFFER_NODE_DECLARE_TYPE( + CSGafferUI::CsVisualiseValueTool, + CSInternalTypes::CsVisualiseValueToolTypeId, + GafferSceneUI::SelectionTool ); + + /** + * @brief Class encapsulating a selected scene location + */ + struct Selection + { + /** + * @brief ctor + * @param scene scene + * @param path scene path + * @param context context + */ + Selection + ( + GafferScene::ScenePlug const& scene, + GafferScene::ScenePlug::ScenePath const& path, + Gaffer::Context const& context + ); + + /** + * @brief Get the scene + * @return scene + */ + GafferScene::ScenePlug const& scene() const; + + /** + * @brief Get the scene path + * @return scene path + */ + GafferScene::ScenePlug::ScenePath const& path() const; + + /** + * @brief Get the context + * @return context + */ + Gaffer::Context const& context() const; + + private: + + GafferScene::ConstScenePlugPtr m_scene; + GafferScene::ScenePlug::ScenePath m_path; + Gaffer::ConstContextPtr m_context; + }; + + /** + * @brief Get the current selection + * @return current selection + */ + std::vector< Selection > const& selection() const; + + /** + * @brief Get the cursor position in raster space + * @return cursor position in raster space + */ + Imath::V2f cursorPos() const; + + /** + * @brief Get the value at current cursor position + * @return value at current cursor position (nullptr if invalid) + */ + IECore::Data const* cursorValue() const; + + private: + + GafferScene::ScenePlug* internalScenePlug(); + GafferScene::ScenePlug const* internalScenePlug() const; + + void connectOnActive(); + void disconnectOnInactive(); + bool mouseMove( GafferUI::ButtonEvent const& event ); + void enter( GafferUI::ButtonEvent const& event ); + void leave( GafferUI::ButtonEvent const& event ); + bool keyPress( GafferUI::KeyEvent const& event ); + bool buttonPress( GafferUI::ButtonEvent const& event ); + bool buttonRelease( GafferUI::ButtonEvent const& event ); + IECore::RunTimeTypedPtr dragBegin( GafferUI::DragDropEvent const& event ); + bool dragEnd( GafferUI::DragDropEvent const& event ); + void plugDirtied( Gaffer::Plug const* plug ); + void plugSet( Gaffer::Plug* plug ); + void metadataChanged( IECore::InternedString const& key ); + void updateSelection() const; + void preRender(); + void updateCursorPos( GafferUI::ButtonEvent const& event ); + void updateCursorValue(); + GafferSceneUI::SceneGadget* sceneGadget(); + GafferSceneUI::SceneGadget const* sceneGadget() const; + +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + void contextChanged(); + void selectedPathsChanged(); +# else + void connectToViewContext(); + void contextChanged( IECore::InternedString const& name ); + Gaffer::Signals::ScopedConnection m_contextChangedConnection; +# endif + Gaffer::Signals::ScopedConnection m_preRenderConnection; + Gaffer::Signals::ScopedConnection m_buttonPressConnection; + Gaffer::Signals::ScopedConnection m_dragBeginConnection; + + GafferUI::GadgetPtr m_gadget; + mutable std::vector< Selection > m_selection; + Imath::V2i m_cursorPos; + bool m_cursorPosValid; + IECore::DataPtr m_cursorValue; + bool m_gadgetDirty; + mutable bool m_selectionDirty; + bool m_priorityPathsDirty; + bool m_acceptedButtonPress; + bool m_initiatedDrag; + + static ToolDescription< CsVisualiseValueTool, GafferSceneUI::SceneView > m_toolDescription; + static size_t m_firstPlugIndex; + }; + +} // CSGafferUI + +#endif // CSGAFFERUI_TOOLS_CSVISUALISEVALUETOOL_H diff --git a/contrib/visualisers/CsVisualiseVectorTool.cpp b/contrib/visualisers/CsVisualiseVectorTool.cpp new file mode 100644 index 00000000000..3d3094cafd2 --- /dev/null +++ b/contrib/visualisers/CsVisualiseVectorTool.cpp @@ -0,0 +1,973 @@ +////////////////////////////////////////////////////////////////////////// +// +// 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +IECORE_PUSH_DEFAULT_VISIBILITY +#if GAFFER_COMPATIBILITY_VERSION < MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 3 ) +#include +#else +#include +#endif +IECORE_POP_DEFAULT_VISIBILITY + +#define private public +#include +#include +#undef private + +#include "CsVisualiseVectorTool.h" + +#include +#include +#include +#include +#include +#include +#if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) +#include +#else +#include +#endif + +#include +#include + +#include +#include +//#include +//#include +//#include +#include +#if ( GAFFER_MILESTONE_VERSION == 0 && GAFFER_MAJOR_VERSION < 61 ) +#include +#endif + +#if GAFFER_COMPATIBILITY_VERSION < MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 3 ) +#include +#else +#include +#endif + +#include +#include +#include + +#include +#include +#include +#include + +namespace +{ + // scale and colour constants + + float const g_scaleDefault = 1.f; + float const g_scaleMin = 10.f * std::numeric_limits< float >::min(); + float const g_scaleInc = 0.01f; + + Imath::Color3f g_colourDefault( 1.f, 1.f, 1.f ); + + // name of P primitive variable + + std::string const g_pName( "P" ); + + // uniform block structure (std140 layout) + + struct UniformBlock + { + alignas( 16 ) Imath::M44f o2v; + alignas( 16 ) Imath::M44f n2v; + alignas( 16 ) Imath::M44f v2c; + alignas( 16 ) Imath::M44f o2c; + alignas( 16 ) Imath::Color3f colour; + alignas( 4 ) float scale; + }; + + GLuint const g_uniformBlockBindingIndex = 0; + +# define UNIFORM_BLOCK_GLSL_SOURCE \ + "layout( std140, row_major ) uniform UniformBlock\n" \ + "{\n" \ + " mat4 o2v;\n" \ + " mat4 n2v;\n" \ + " mat4 v2c;\n" \ + " mat4 o2c;\n" \ + " vec3 colour;\n" \ + " float scale;\n" \ + "} uniforms;\n" + +# define ATTRIB_GLSL_LOCATION_PS 0 +# define ATTRIB_GLSL_LOCATION_VS 1 + +# define ATTRIB_GLSL_SOURCE \ + "layout( location = " BOOST_PP_STRINGIZE( ATTRIB_GLSL_LOCATION_PS ) " ) in vec3 ps;\n" \ + "layout( location = " BOOST_PP_STRINGIZE( ATTRIB_GLSL_LOCATION_VS ) " ) in vec3 vs;\n" + + // opengl vertex shader code (point format) + + std::string const g_vertSourcePoint + ( + "#version 330\n" + + UNIFORM_BLOCK_GLSL_SOURCE + + ATTRIB_GLSL_SOURCE + + "void main()\n" + "{\n" + " vec3 position = ps;\n" + + " if( gl_VertexID == 1 )\n" + " {\n" + " position = vs;\n" + " }\n" + + " gl_Position = vec4( position, 1.0 ) * uniforms.o2c;\n" + "}\n" + ); + + // opengl vertex shader code (vector format) + + std::string const g_vertSourceVector + ( + "#version 330\n" + + UNIFORM_BLOCK_GLSL_SOURCE + + ATTRIB_GLSL_SOURCE + + "void main()\n" + "{\n" + " vec3 position = ps;\n" + + " if( gl_VertexID == 1 )\n" + " {\n" + " position += vs * uniforms.scale;" + " }\n" + + " gl_Position = vec4( position, 1.0 ) * uniforms.o2c;\n" + "}\n" + ); + + // opengl vertex shader code (bivector format) + + std::string const g_vertSourceBivector + ( + "#version 330\n" + + UNIFORM_BLOCK_GLSL_SOURCE + + ATTRIB_GLSL_SOURCE + + "void main()\n" + "{\n" + " vec4 position = vec4( ps, 1.0 ) * uniforms.o2v;\n" + + " if( gl_VertexID == 1 )\n" + " {\n" + " position.xyz += normalize( vs * mat3( uniforms.n2v ) ) * ( uniforms.scale * length( vs ) );\n" + " }\n" + + " gl_Position = position * uniforms.v2c;\n" + "}\n" + ); + + // opengl fragment shader code + + std::string const g_fragSource + ( + "#version 330\n" + + UNIFORM_BLOCK_GLSL_SOURCE + + "layout( location = 0 ) out vec4 cs;\n" + + "void main()\n" + "{\n" + " cs = vec4( uniforms.colour, 1.0 );\n" + "}\n" + ); + + // the gadget that does the actual opengl drawing of the vector lines + + struct Gadget + : public GafferUI::Gadget + { + explicit + Gadget + ( + CSGafferUI::CsVisualiseVectorTool const& tool, + std::string const& name = "CsVisualiseVectorGadget" + ) + : GafferUI::Gadget( name ) + , m_tool( & tool ) + , m_vectorShader() + , m_bivectorShader() + , m_uniformBuffer() + {} + + void resetTool() + { + m_tool = nullptr; + } + + protected: + + void +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 61 ) + renderLayer +# else + doRenderLayer +# endif + ( + GafferUI::Gadget::Layer layer, + GafferUI::Style const* style +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 61 ) + , GafferUI::Gadget::RenderReason reason +# endif + ) + const override + { + if( ( layer != GafferUI::Gadget::Layer::MidFront ) || +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 61 ) + ( GafferUI::Gadget::isSelectionRender( reason ) ) ) +# else + ( IECoreGL::Selector::currentSelector() ) ) +# endif + { + return; + } + + // check tool reference valid + + if( m_tool == nullptr ) + { + return; + } + + // get parent viewport gadget + + GafferUI::ViewportGadget const* const viewportGadget = + ancestor< GafferUI::ViewportGadget >(); + if( viewportGadget == nullptr ) + { + return; + } + + // bootleg shaders + + buildShader( m_pointShader, g_vertSourcePoint ); + buildShader( m_vectorShader, g_vertSourceVector ); + buildShader( m_bivectorShader, g_vertSourceBivector ); + + // get the cached converter from IECoreGL, this is used to convert primitive + // variable data to opengl buffers which will be shared with the IECoreGL renderer + + IECoreGL::CachedConverter* const converter = + IECoreGL::CachedConverter::defaultCachedConverter(); + + // bootleg uniform buffer + + GLint uniformBinding; + glGetIntegerv( GL_UNIFORM_BUFFER_BINDING, & uniformBinding ); + + if( ! m_uniformBuffer ) + { + GLuint buffer = 0u; + glGenBuffers( 1, & buffer ); + glBindBuffer( GL_UNIFORM_BUFFER, buffer ); + glBufferData( GL_UNIFORM_BUFFER, sizeof( UniformBlock ), 0, GL_DYNAMIC_DRAW ); + m_uniformBuffer.reset( new IECoreGL::Buffer( buffer ) ); + } + + glBindBufferBase( GL_UNIFORM_BUFFER, g_uniformBlockBindingIndex, m_uniformBuffer->m_buffer ); + + // get the name of the primitive variable to visualise and format + + std::string const& name = m_tool->namePlug()->getValue(); + CSGafferUI::CsVisualiseVectorTool::Format const format = + static_cast< CSGafferUI::CsVisualiseVectorTool::Format >( + m_tool->formatPlug()->getValue() ); + + // get scale factor and colour + + UniformBlock uniforms; + uniforms.colour = m_tool->colourPlug()->getValue(); + uniforms.scale = m_tool->scalePlug()->getValue(); + + // get the world to view and view to clip space matrices + + Imath::M44f const w2v = viewportGadget->getCameraTransform().gjInverse(); + glGetFloatv( GL_PROJECTION_MATRIX, uniforms.v2c.getValue() ); + + // set opengl state + + GLfloat lineWidth; + glGetFloatv( GL_LINE_WIDTH, & lineWidth ); + glLineWidth( 1.f ); + + GLboolean const depthEnabled = glIsEnabled( GL_DEPTH_TEST ); + if( ! depthEnabled ) glEnable( GL_DEPTH_TEST ); + + GLboolean depthWriteEnabled; + glGetBooleanv( GL_DEPTH_WRITEMASK, & depthWriteEnabled ); + if( depthWriteEnabled ) glDepthMask( GL_FALSE ); + + GLboolean lineSmooth; + glGetBooleanv( GL_LINE_SMOOTH, & lineSmooth ); + if( lineSmooth ) glDisable( GL_LINE_SMOOTH ); + + GLboolean const blendEnabled = glIsEnabled( GL_BLEND ); + if( blendEnabled ) glDisable( GL_BLEND ); + + // choose shader program + + GLint shaderProgram; + glGetIntegerv( GL_CURRENT_PROGRAM, & shaderProgram ); + glUseProgram( chooseProgram( format ) ); + + // set opengl vertex attribute array state + + GLint arrayBinding; + glGetIntegerv( GL_ARRAY_BUFFER_BINDING, & arrayBinding ); + + glPushClientAttrib( GL_CLIENT_VERTEX_ARRAY_BIT ); + + glVertexAttribDivisor( ATTRIB_GLSL_LOCATION_PS, 1 ); + glEnableVertexAttribArray( ATTRIB_GLSL_LOCATION_PS ); + glVertexAttribDivisor( ATTRIB_GLSL_LOCATION_VS, 1 ); + glEnableVertexAttribArray( ATTRIB_GLSL_LOCATION_VS ); + + // loop through current selection + + for( std::vector< CSGafferUI::CsVisualiseVectorTool::Selection >::const_iterator + it = m_tool->selection().begin(), + itEnd = m_tool->selection().end(); it != itEnd; ++it ) + { + GafferScene::ScenePlug::PathScope scope( &( ( *it ).context() ), &( ( *it ).path() ) ); + + // check path exists + + if( !( ( *it ).scene().existsPlug()->getValue() ) ) + { + continue; + } + + // extract primitive + + IECoreScene::ConstPrimitivePtr const primitive = + IECore::runTimeCast< IECoreScene::Primitive const >( + ( *it ).scene().objectPlug()->getValue() ); + + if( ! primitive ) + { + continue; + } + + // retrieve cached IECoreGL primitive + + IECoreGL::ConstPrimitivePtr const primitiveGL = + IECore::runTimeCast< IECoreGL::Primitive const >( + converter->convert( primitive.get() ) ); + + if( ! primitiveGL ) + { + continue; + } + + // find "P" vertex attribute + + IECoreGL::Primitive::AttributeMap::const_iterator const pit = + primitiveGL->m_vertexAttributes.find( g_pName ); + if( pit == primitiveGL->m_vertexAttributes.end() ) + { + continue; + } + + IECore::ConstV3fVectorDataPtr const pData = + IECore::runTimeCast< IECore::V3fVectorData const >( ( *pit ).second ); + if( ! pData ) + { + continue; + } + + // find named vertex attribute + // + // NOTE : conversion to IECoreGL mesh may generate vertex attributes (eg. "N") + // so check named primitive variable exists on IECore mesh primitive as well. + + IECoreGL::Primitive::AttributeMap::const_iterator const vit = + primitiveGL->m_vertexAttributes.find( name ); + if( ( vit == primitiveGL->m_vertexAttributes.end() ) || + ( primitive->variables.find( name ) == primitive->variables.end() ) ) + { + continue; + } + + IECore::ConstV3fVectorDataPtr const vData = + IECore::runTimeCast< IECore::V3fVectorData const >( ( *vit ).second ); + if( ! vData ) + { + continue; + } + + // retrieve cached opengl buffer data + + IECoreGL::ConstBufferPtr const pBuffer = + IECore::runTimeCast< IECoreGL::Buffer const >( converter->convert( pData.get() ) ); + IECoreGL::ConstBufferPtr const vBuffer = + IECore::runTimeCast< IECoreGL::Buffer const >( converter->convert( vData.get() ) ); + + // get the object to world transform + + Imath::M44f o2w; + GafferScene::ScenePlug::ScenePath path( ( *it ).path() ); + while( ! path.empty() ) + { + scope.setPath( & path ); + o2w = o2w * ( *it ).scene().transformPlug()->getValue(); + path.pop_back(); + } + + // compute object/normal to view and object to clip matrices + + uniforms.o2v = o2w * w2v; + uniforms.n2v = ( uniforms.o2v.gjInverse() ).transpose(); + uniforms.o2c = uniforms.o2v * uniforms.v2c; + + // upload opengl uniform block data + + glBufferData( GL_UNIFORM_BUFFER, sizeof( UniformBlock ), & uniforms, GL_DYNAMIC_DRAW ); + + // instance a line segment for each element of vector data + + glBindBuffer( GL_ARRAY_BUFFER, pBuffer->m_buffer ); + glVertexAttribPointer( ATTRIB_GLSL_LOCATION_PS, 3, GL_FLOAT, GL_FALSE, 0, + static_cast< void const* >( 0 ) ); + glBindBuffer( GL_ARRAY_BUFFER, vBuffer->m_buffer ); + glVertexAttribPointer( ATTRIB_GLSL_LOCATION_VS, 3, GL_FLOAT, GL_FALSE, 0, + static_cast< void const* >( 0 ) ); + glDrawArraysInstanced( GL_LINES, 0, 2, static_cast< GLsizei >( pData->readable().size() ) ); + } + + // restore opengl state + + glPopClientAttrib(); + glBindBuffer( GL_ARRAY_BUFFER, arrayBinding ); + glBindBuffer( GL_UNIFORM_BUFFER, uniformBinding ); + + glLineWidth( lineWidth ); + + if( lineSmooth ) glEnable( GL_LINE_SMOOTH ); + if( blendEnabled ) glEnable( GL_BLEND ); + if( ! depthEnabled ) glDisable( GL_DEPTH_TEST ); + if( depthWriteEnabled ) glDepthMask( GL_TRUE ); + glUseProgram( shaderProgram ); + } + +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 61 ) + Imath::Box3f renderBound() const override + { + // NOTE : for now just return an infinite box + + Imath::Box3f b; + b.makeInfinite(); + return b; + } +# endif + +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 61 ) + unsigned layerMask() const override + { + return ( m_tool ) + ? static_cast< unsigned >( GafferUI::Gadget::Layer::MidFront ) + : static_cast< unsigned >( 0 ); + } +# else + bool hasLayer( GafferUI::Gadget::Layer layer ) + { + return ( m_tool && + ( layer == GafferUI::Gadget::Layer::MidFront ) ); + } +# endif + + private: + + GLuint + chooseProgram + ( + CSGafferUI::CsVisualiseVectorTool::Format const format + ) + const + { + IECoreGL::Shader const* shader = nullptr; + switch( format ) + { + case CSGafferUI::CsVisualiseVectorTool::Format::Point: + shader = m_pointShader.get(); + break; + case CSGafferUI::CsVisualiseVectorTool::Format::Vector: + shader = m_vectorShader.get(); + break; + case CSGafferUI::CsVisualiseVectorTool::Format::Bivector: + shader = m_bivectorShader.get(); + break; + default: + assert( 0 ); + break; + } + + return ( shader != nullptr ) ? shader->program() : static_cast< GLuint >( 0 ); + } + + void + buildShader + ( + IECoreGL::ConstShaderPtr& shader, + std::string const& vertSource + ) + const + { + if( ! shader ) + { + shader = IECoreGL::ShaderLoader::defaultShaderLoader()->create( + vertSource, std::string(), g_fragSource ); + if( shader ) + { + GLuint const program = shader->program(); + GLuint const blockIndex = glGetUniformBlockIndex( program, "UniformBlock" ); + if( blockIndex != GL_INVALID_INDEX ) + { + glUniformBlockBinding( program, blockIndex, g_uniformBlockBindingIndex ); + } + } + } + } + + CSGafferUI::CsVisualiseVectorTool const* m_tool; + mutable IECoreGL::ConstShaderPtr m_pointShader; + mutable IECoreGL::ConstShaderPtr m_vectorShader; + mutable IECoreGL::ConstShaderPtr m_bivectorShader; + mutable IECoreGL::ConstBufferPtr m_uniformBuffer; + }; + +} // namespace + +namespace CSGafferUI +{ + GAFFER_NODE_DEFINE_TYPE( CsVisualiseVectorTool ) + + GafferUI::Tool::ToolDescription< CsVisualiseVectorTool, GafferSceneUI::SceneView > CsVisualiseVectorTool::m_toolDescription; + + size_t CsVisualiseVectorTool::m_firstPlugIndex = 0; + + CsVisualiseVectorTool::CsVisualiseVectorTool + ( + GafferSceneUI::SceneView* const view, + std::string const& name + ) + : GafferSceneUI::SelectionTool( view, name ) +# if GAFFER_COMPATIBILITY_VERSION < MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + , m_contextChangedConnection() +# endif + , m_preRenderConnection() + , m_gadget( new Gadget( *this ) ) + , m_selection() + , m_gadgetDirty( true ) + , m_selectionDirty( true ) + , m_priorityPathsDirty( true ) + { + // add gadget to view and hide + + view->viewportGadget()->addChild( m_gadget ); + m_gadget->setVisible( false ); + + // store offset of first plug + + storeIndexOfNextChild( m_firstPlugIndex ); + + // add child plugs + + addChild( new Gaffer::StringPlug( "name", Gaffer::Plug::In, "N" ) ); + addChild( new Gaffer::IntPlug( "format", Gaffer::Plug::In, + static_cast< int >( Format::Bivector ), + static_cast< int >( Format::Point ), + static_cast< int >( Format::Bivector ) ) ); + addChild( new Gaffer::FloatPlug( "scale", Gaffer::Plug::In, g_scaleDefault, g_scaleMin ) ); + addChild( new Gaffer::Color3fPlug( "colour", Gaffer::Plug::In, g_colourDefault ) ); + addChild( new GafferScene::ScenePlug( "__scene", Gaffer::Plug::In ) ); + + // connect out internal scene plug to the parent view's scene plug + + internalScenePlug()->setInput( view->inPlug< GafferScene::ScenePlug >() ); + + // connect signal handlers + + view->viewportGadget()->keyPressSignal().connect( + boost::bind( & CsVisualiseVectorTool::keyPress, this, boost::placeholders::_2 ) ); + + plugDirtiedSignal().connect( + boost::bind( & CsVisualiseVectorTool::plugDirtied, this, boost::placeholders::_1 ) ); + +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + view->contextChangedSignal().connect( + boost::bind( & CsVisualiseVectorTool::contextChanged, this ) ); + GafferSceneUI::ScriptNodeAlgo::selectedPathsChangedSignal( view->scriptNode() ).connect( + boost::bind( &CsVisualiseVectorTool::selectedPathsChanged, this ) ); +# else + connectToViewContext(); + view->contextChangedSignal().connect( + boost::bind( & CsVisualiseVectorTool::connectToViewContext, this ) ); +# endif + + Gaffer::Metadata::plugValueChangedSignal().connect( + boost::bind( & CsVisualiseVectorTool::metadataChanged, this, boost::placeholders::_3 ) ); + Gaffer::Metadata::nodeValueChangedSignal().connect( + boost::bind( & CsVisualiseVectorTool::metadataChanged, this, boost::placeholders::_2 ) ); + } + + CsVisualiseVectorTool::~CsVisualiseVectorTool() + { + // NOTE : ensure that the gadget's reference to the tool is reset + + static_cast< Gadget* >( m_gadget.get() )->resetTool(); + } + + Gaffer::StringPlug* CsVisualiseVectorTool::namePlug() + { + return const_cast< Gaffer::StringPlug* >( + static_cast< CsVisualiseVectorTool const* >( this )->namePlug() ); + } + + Gaffer::StringPlug const* CsVisualiseVectorTool::namePlug() const + { + return getChild< Gaffer::StringPlug >( m_firstPlugIndex + 0 ); + } + + Gaffer::IntPlug* CsVisualiseVectorTool::formatPlug() + { + return const_cast< Gaffer::IntPlug* >( + static_cast< CsVisualiseVectorTool const* >( this )->formatPlug() ); + } + + Gaffer::IntPlug const* CsVisualiseVectorTool::formatPlug() const + { + return getChild< Gaffer::IntPlug >( m_firstPlugIndex + 1 ); + } + + Gaffer::FloatPlug* CsVisualiseVectorTool::scalePlug() + { + return const_cast< Gaffer::FloatPlug* >( + static_cast< CsVisualiseVectorTool const* >( this )->scalePlug() ); + } + + Gaffer::FloatPlug const* CsVisualiseVectorTool::scalePlug() const + { + return getChild< Gaffer::FloatPlug >( m_firstPlugIndex + 2 ); + } + + Gaffer::Color3fPlug* CsVisualiseVectorTool::colourPlug() + { + return const_cast< Gaffer::Color3fPlug* >( + static_cast< CsVisualiseVectorTool const* >( this )->colourPlug() ); + } + + Gaffer::Color3fPlug const* CsVisualiseVectorTool::colourPlug() const + { + return getChild< Gaffer::Color3fPlug >( m_firstPlugIndex + 3 ); + } + + GafferScene::ScenePlug* CsVisualiseVectorTool::internalScenePlug() + { + return const_cast< GafferScene::ScenePlug* >( + static_cast< CsVisualiseVectorTool const* >( this )->internalScenePlug() ); + } + + GafferScene::ScenePlug const* CsVisualiseVectorTool::internalScenePlug() const + { + return getChild< GafferScene::ScenePlug >( m_firstPlugIndex + 4 ); + } + + std::vector< CsVisualiseVectorTool::Selection > const& CsVisualiseVectorTool::selection() const + { + return m_selection; + } +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + void CsVisualiseVectorTool::contextChanged() + { + // Context changes can change the scene, which in turn + // dirties our selection. + selectedPathsChanged(); + } + + void CsVisualiseVectorTool::selectedPathsChanged() + { + m_selectionDirty = true; + m_gadgetDirty = true; + m_priorityPathsDirty = true; + } +# else + void CsVisualiseVectorTool::connectToViewContext() + { + m_contextChangedConnection = view()->getContext()->changedSignal().connect( + boost::bind( & CsVisualiseVectorTool::contextChanged, this, boost::placeholders::_2 ) ); + } + + void CsVisualiseVectorTool::contextChanged + ( + IECore::InternedString const& name + ) + { + if( GafferSceneUI::ContextAlgo::affectsSelectedPaths( name ) || + GafferSceneUI::ContextAlgo::affectsLastSelectedPath( name ) || + ! boost::starts_with( name.string(), "ui:" ) ) + { + m_selectionDirty = true; + m_gadgetDirty = true; + m_priorityPathsDirty = true; + } + } +# endif + + void CsVisualiseVectorTool::plugDirtied + ( + Gaffer::Plug const* const plug + ) + { + if( ( plug == activePlug() ) || + ( plug == internalScenePlug()->objectPlug() ) || + ( plug == internalScenePlug()->transformPlug() ) ) + { + m_selectionDirty = true; + m_gadgetDirty = true; + m_priorityPathsDirty = true; + } + else + if( ( plug == namePlug() ) || + ( plug == scalePlug() ) || + ( plug == colourPlug() ) || + ( plug == formatPlug() ) ) + { + m_gadgetDirty = true; + view()->viewportGadget()->renderRequestSignal()( view()->viewportGadget() ); + } + + if( plug == activePlug() ) + { + if( activePlug()->getValue() ) + { + m_preRenderConnection = view()->viewportGadget()->preRenderSignal().connect( + boost::bind( & CsVisualiseVectorTool::preRender, this ) ); + } + else + { + m_preRenderConnection.disconnect(); + m_gadget->setVisible( false ); + + static_cast< GafferSceneUI::SceneGadget* >( view()->viewportGadget()->getPrimaryChild() ) + ->setPriorityPaths( IECore::PathMatcher() ); + } + } + } + + void CsVisualiseVectorTool::metadataChanged + ( + IECore::InternedString const& key + ) + { + if( ! Gaffer::MetadataAlgo::readOnlyAffectedByChange( key ) ) + { + return; + } + + if( ! m_selectionDirty ) + { + m_selectionDirty = true; + } + + if( ! m_gadgetDirty ) + { + m_gadgetDirty = true; + view()->viewportGadget()->renderRequestSignal()( view()->viewportGadget() ); + } + } + + void CsVisualiseVectorTool::updateSelection() const + { + if( ! m_selectionDirty ) + { + return; + } + + m_selection.clear(); + m_selectionDirty = false; + + if( ! activePlug()->getValue() ) + { + return; + } + + GafferScene::ScenePlug const* scene = + internalScenePlug()->getInput< GafferScene::ScenePlug >(); + + if( !( scene ) || + !( scene = scene->getInput< GafferScene::ScenePlug >() ) ) + { + return; + } + +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + IECore::PathMatcher const selectedPaths = + GafferSceneUI::ScriptNodeAlgo::getSelectedPaths( view()->scriptNode() ); +# else + IECore::PathMatcher const selectedPaths = + GafferSceneUI::ContextAlgo::getSelectedPaths( view()->getContext() ); +# endif + + if( selectedPaths.isEmpty() ) + { + return; + } + + for( IECore::PathMatcher::Iterator it = selectedPaths.begin(), + itEnd = selectedPaths.end(); it != itEnd; ++it ) + { +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + m_selection.emplace_back( *( scene ), *it, *( view()->context() ) ); +# else + m_selection.emplace_back( *( scene ), *it, *( view()->getContext() ) ); +# endif + } + } + + void CsVisualiseVectorTool::preRender() + { + updateSelection(); + + if( m_priorityPathsDirty ) + { +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + static_cast< GafferSceneUI::SceneGadget* >( view()->viewportGadget()->getPrimaryChild() ) + ->setPriorityPaths( ( m_selection.empty() ) + ? IECore::PathMatcher() + : GafferSceneUI::ScriptNodeAlgo::getSelectedPaths( view()->scriptNode() ) ); +# else + static_cast< GafferSceneUI::SceneGadget* >( view()->viewportGadget()->getPrimaryChild() ) + ->setPriorityPaths( ( m_selection.empty() ) + ? IECore::PathMatcher() + : GafferSceneUI::ContextAlgo::getSelectedPaths( view()->getContext() ) ); +# endif + m_priorityPathsDirty = false; + } + + if( m_selection.empty() ) + { + m_gadget->setVisible( false ); + return; + } + + m_gadget->setVisible( true ); + + if( m_gadgetDirty ) + { + m_gadgetDirty = false; + } + } + + bool CsVisualiseVectorTool::keyPress + ( + GafferUI::KeyEvent const& event + ) + { + if( ! activePlug()->getValue() ) + { + return false; + } + + // allow user to scale vectors with +/- keys + + if( event.key == "Plus" || event.key == "Equal" ) + { + scalePlug()->setValue( scalePlug()->getValue() + g_scaleInc ); + } + else + if( event.key == "Minus" || event.key == "Underscore" ) + { + scalePlug()->setValue( std::max( scalePlug()->getValue() - g_scaleInc, g_scaleMin ) ); + } + + return false; + } + + CsVisualiseVectorTool::Selection::Selection + ( + GafferScene::ScenePlug const& scene, + GafferScene::ScenePlug::ScenePath const& path, + Gaffer::Context const& context + ) + : m_scene( & scene ) + , m_path( path ) + , m_context( & context ) + {} + + GafferScene::ScenePlug const& CsVisualiseVectorTool::Selection::scene() const + { + return *( m_scene ); + } + + GafferScene::ScenePlug::ScenePath const& CsVisualiseVectorTool::Selection::path() const + { + return m_path; + } + + Gaffer::Context const& CsVisualiseVectorTool::Selection::context() const + { + return *( m_context ); + } + +} // CSGafferUI diff --git a/contrib/visualisers/CsVisualiseVectorTool.h b/contrib/visualisers/CsVisualiseVectorTool.h new file mode 100644 index 00000000000..07ad3a543dc --- /dev/null +++ b/contrib/visualisers/CsVisualiseVectorTool.h @@ -0,0 +1,195 @@ +////////////////////////////////////////////////////////////////////////// +// +// 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. +// +////////////////////////////////////////////////////////////////////////// + +#ifndef CSGAFFERUI_TOOLS_CSVISUALISEVECTORTOOL_H +#define CSGAFFERUI_TOOLS_CSVISUALISEVECTORTOOL_H + +#include "../../GafferTypeIds.h" + +#include +#include +#include +#include +#include +#include + +namespace CSGafferUI +{ + /** + * @brief Tool that displays a named primitive variable of type Imath::V3f as line vectors + * + * The data is interpreted and visualised based on the format plug. + */ + struct CsVisualiseVectorTool + : public GafferSceneUI::SelectionTool + { + /** + * @brief Data formats + */ + enum class Format + { + Point = 0, /**< Interpret data as points */ + Vector, /**< Interpret data as vectors */ + Bivector /**< Interpret data as bivectors */ + }; + + /** + * @brief ctor + * @param view parent view + * @param name name for node + */ + explicit + CsVisualiseVectorTool + ( + GafferSceneUI::SceneView* view, + std::string const& name = Gaffer::GraphComponent::defaultName< CsVisualiseVectorTool >() + ); + + /** + * @brief dtor + */ + ~CsVisualiseVectorTool() override; + + /** + * @name GafferPlugAccessors + * @brief Gaffer plug accessor functions + * @{ + */ + + Gaffer::StringPlug* namePlug(); + Gaffer::StringPlug const* namePlug() const; + + Gaffer::IntPlug* formatPlug(); + Gaffer::IntPlug const* formatPlug() const; + + Gaffer::FloatPlug* scalePlug(); + Gaffer::FloatPlug const* scalePlug() const; + + Gaffer::Color3fPlug* colourPlug(); + Gaffer::Color3fPlug const* colourPlug() const; + + /** + * @} + */ + + GAFFER_NODE_DECLARE_TYPE( + CSGafferUI::CsVisualiseVectorTool, + CSInternalTypes::CsVisualiseVectorToolTypeId, + GafferSceneUI::SelectionTool ); + + /** + * @brief Class encapsulating a selected scene location + */ + struct Selection + { + /** + * @brief ctor + * @param scene scene + * @param path scene path + * @param context context + */ + Selection + ( + GafferScene::ScenePlug const& scene, + GafferScene::ScenePlug::ScenePath const& path, + Gaffer::Context const& context + ); + + /** + * @brief Get the scene + * @return scene + */ + GafferScene::ScenePlug const& scene() const; + + /** + * @brief Get the scene path + * @return scene path + */ + GafferScene::ScenePlug::ScenePath const& path() const; + + /** + * @brief Get the context + * @return context + */ + Gaffer::Context const& context() const; + + private: + + GafferScene::ConstScenePlugPtr m_scene; + GafferScene::ScenePlug::ScenePath m_path; + Gaffer::ConstContextPtr m_context; + }; + + /** + * @brief Get the current selection + * @return current selection + */ + std::vector< Selection > const& selection() const; + + private: + + GafferScene::ScenePlug* internalScenePlug(); + GafferScene::ScenePlug const* internalScenePlug() const; + + void plugDirtied( Gaffer::Plug const* plug ); + void metadataChanged( IECore::InternedString const& key ); + void updateSelection() const; + void preRender(); + bool keyPress( GafferUI::KeyEvent const& event ); + +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + void contextChanged(); + void selectedPathsChanged(); +# else + void connectToViewContext(); + void contextChanged( IECore::InternedString const& name ); + Gaffer::Signals::ScopedConnection m_contextChangedConnection; +# endif + Gaffer::Signals::ScopedConnection m_preRenderConnection; + + GafferUI::GadgetPtr m_gadget; + mutable std::vector< Selection > m_selection; + bool m_gadgetDirty; + mutable bool m_selectionDirty; + bool m_priorityPathsDirty; + + static ToolDescription< CsVisualiseVectorTool, GafferSceneUI::SceneView > m_toolDescription; + static size_t m_firstPlugIndex; + }; + +} // CSGafferUI + +#endif // CSGAFFERUI_TOOLS_CSVISUALISEVECTORTOOL_H diff --git a/contrib/visualisers/CsVisualiseVertexIdTool.cpp b/contrib/visualisers/CsVisualiseVertexIdTool.cpp new file mode 100644 index 00000000000..b1cd53fddcb --- /dev/null +++ b/contrib/visualisers/CsVisualiseVertexIdTool.cpp @@ -0,0 +1,1373 @@ +////////////////////////////////////////////////////////////////////////// +// +// 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +IECORE_PUSH_DEFAULT_VISIBILITY +#if GAFFER_COMPATIBILITY_VERSION < MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 3 ) +#include +#else +#include +#endif +IECORE_POP_DEFAULT_VISIBILITY + +#define private public +#include +#include +#undef private + +#include "CsVisualiseVertexIdTool.h" + +#include +#include +#include +#include +#include +#include +#include +#if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) +#include +#else +#include +#endif + +#include +#include + +#include +#include +#include +//#include +//#include +//#include +#include +#if ( GAFFER_MILESTONE_VERSION == 0 && GAFFER_MAJOR_VERSION < 61 ) +#include +#endif + +//#include + +#include +#include + +#include +#include +#include + +namespace +{ + // text sizes + + float const g_textSizeDefault = 9.0f; + float const g_textSizeMin = 6.0f; + float const g_textSizeInc = 0.5f; + float const g_cursorRadiusDefault = 25.f; + + Imath::Color3f const g_colourFGDefault( 0.9f ); + Imath::Color3f const g_colourHLDefault( 0.466f, 0.612f, 0.741f ); + + // convert three component colour to four component colour with full opacity + + Imath::Color4f convertToColor4f + ( + Imath::Color3f const& c + ) + { + return Imath::Color4f( c[ 0 ], c[ 1 ], c[ 2 ], 1.f ); + } + + // name of P primitive variable + + std::string const g_pName( "P" ); + + // uniform block structure (std140 layout) + + struct UniformBlock + { + alignas( 16 ) Imath::M44f o2c; + }; + + // block binding indexes for the uniform and shader storage buffers + + GLuint const g_uniformBlockBindingIndex = 0; + GLuint const g_storageBlockBindingIndex = 0; + + // uniform block definition (std140 layout) + +# define UNIFORM_BLOCK_NAME "UniformBlock" +# define UNIFORM_BLOCK_GLSL_SOURCE \ + "layout( std140, row_major ) uniform " UNIFORM_BLOCK_NAME "\n" \ + "{\n" \ + " mat4 o2c;\n" \ + "} uniforms;\n" + + // shader storage block definition (std430 layout) + // + // NOTE : std430 layout ensures that the elements of a uint array are tightly packed + // std140 would require 16 byte alignment of each element ... + +# define STORAGE_BLOCK_NAME "StorageBlock" +# define STORAGE_BLOCK_GLSL_SOURCE \ + "layout( std430 ) buffer " STORAGE_BLOCK_NAME "\n" \ + "{\n" \ + " coherent restrict uint visibility[];\n" \ + "} buffers;\n" + + // vertex attribute definitions + +# define ATTRIB_GLSL_LOCATION_PS 0 +# define ATTRIB_GLSL_SOURCE \ + "layout( location = " BOOST_PP_STRINGIZE( ATTRIB_GLSL_LOCATION_PS ) " ) in vec3 ps;\n" + + // interface block definition + +# define INTERFACE_BLOCK_GLSL_SOURCE( STORAGE, NAME ) \ + BOOST_PP_STRINGIZE( STORAGE ) " InterfaceBlock\n" \ + "{\n" \ + " flat uint vertexId;\n" \ + "} " BOOST_PP_STRINGIZE( NAME ) ";\n" + + // opengl vertex shader code + + std::string const g_vertSource + ( + "#version 430\n" + + UNIFORM_BLOCK_GLSL_SOURCE + + ATTRIB_GLSL_SOURCE + + INTERFACE_BLOCK_GLSL_SOURCE( out, outputs ) + + "void main()\n" + "{\n" + " gl_Position = vec4( ps, 1.0 ) * uniforms.o2c;\n" + " outputs.vertexId = uint( gl_VertexID );\n" + "}\n" + ); + + // opengl fragment shader code + + std::string const g_fragSource + ( + "#version 430\n" + + // NOTE : ensure that shader is only run for fragments that pass depth test. + + "layout( early_fragment_tests ) in;\n" + + STORAGE_BLOCK_GLSL_SOURCE + + UNIFORM_BLOCK_GLSL_SOURCE + + INTERFACE_BLOCK_GLSL_SOURCE( in, inputs ) + + "void main()\n" + "{\n" + " uint index = inputs.vertexId / 32u;\n" + " uint value = inputs.vertexId % 32u;\n" + " atomicOr( buffers.visibility[ index ], 1u << value );\n" + "}\n" + ); + + // the gadget that does the actual opengl drawing of the vertex id text + + struct Gadget + : public GafferUI::Gadget + { + explicit + Gadget + ( + CSGafferUI::CsVisualiseVertexIdTool& tool, + std::string const& name = "CsVisualiseVertexIdGadget" + ) + : GafferUI::Gadget( name ) + , m_tool( & tool ) + , m_shader() + , m_uniformBuffer() + , m_storageBuffer() + , m_storageCapacity( 0 ) + {} + + void resetTool() + { + m_tool = nullptr; + } + + protected: + + void +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 61 ) + renderLayer +# else + doRenderLayer +# endif + ( + GafferUI::Gadget::Layer layer, + GafferUI::Style const* style +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 61 ) + , GafferUI::Gadget::RenderReason reason +# endif + ) + const override + { + if( ( layer != GafferUI::Gadget::Layer::MidFront ) || +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 61 ) + ( GafferUI::Gadget::isSelectionRender( reason ) ) ) +# else + ( IECoreGL::Selector::currentSelector() ) ) +# endif + { + return; + } + + // check tool reference valid + + if( m_tool == nullptr ) + { + return; + } + + // get parent viewport gadget + + GafferUI::ViewportGadget const* const viewportGadget = + ancestor< GafferUI::ViewportGadget >(); + if( viewportGadget == nullptr ) + { + return; + } + + // bootleg shader + + buildShader(); + + if( ! m_shader ) + { + return; + } + + // get the cached converter from IECoreGL, this is used to convert primitive + // variable data to opengl buffers which will be shared with the IECoreGL renderer + + IECoreGL::CachedConverter* const converter = + IECoreGL::CachedConverter::defaultCachedConverter(); + + // bootleg uniform buffer + + GLint uniformBinding; + glGetIntegerv( GL_UNIFORM_BUFFER_BINDING, & uniformBinding ); + + if( ! m_uniformBuffer ) + { + GLuint buffer = 0u; + glGenBuffers( 1, & buffer ); + glBindBuffer( GL_UNIFORM_BUFFER, buffer ); + glBufferData( GL_UNIFORM_BUFFER, sizeof( UniformBlock ), 0, GL_DYNAMIC_DRAW ); + glBindBuffer( GL_UNIFORM_BUFFER, uniformBinding ); + m_uniformBuffer.reset( new IECoreGL::Buffer( buffer ) ); + } + + UniformBlock uniforms; + + // bootleg storage buffer + + GLint storageBinding; + glGetIntegerv( GL_SHADER_STORAGE_BUFFER_BINDING, & storageBinding ); + + if( ! m_storageBuffer ) + { + GLuint buffer = 0u; + glGenBuffers( 1, & buffer ); + m_storageBuffer.reset( new IECoreGL::Buffer( buffer ) ); + } + + // save opengl state + + GLfloat pointSize; + glGetFloatv( GL_POINT_SIZE, & pointSize ); + + GLint depthFunc; + glGetIntegerv( GL_DEPTH_FUNC, & depthFunc ); + + GLboolean depthWriteEnabled; + glGetBooleanv( GL_DEPTH_WRITEMASK, & depthWriteEnabled ); + + GLboolean const depthEnabled = glIsEnabled( GL_DEPTH_TEST ); + GLboolean const multisampleEnabled = glIsEnabled( GL_MULTISAMPLE ); + + GLint shaderProgram; + glGetIntegerv( GL_CURRENT_PROGRAM, & shaderProgram ); + + GLint arrayBinding; + glGetIntegerv( GL_ARRAY_BUFFER_BINDING, & arrayBinding ); + + // get the world to clip space matrix + + Imath::M44f v2c; + glGetFloatv( GL_PROJECTION_MATRIX, v2c.getValue() ); + Imath::M44f const w2c = viewportGadget->getCameraTransform().gjInverse() * v2c; + + // get raster space bounding box + + Imath::Box2f const rasterBounds = Imath::Box2f( Imath::V2f( 0.f ), + Imath::V2f( static_cast< float >( viewportGadget->getViewport().x ), + static_cast< float >( viewportGadget->getViewport().y ) ) ); + + // get text raster space scale and colour + // + // NOTE : It seems that Gaffer defines the origin of raster space as the top left corner + // of the viewport, however the style text drawing functions assume that y increases + // "up" the screen rather than "down", so invert y to ensure text is not upside down. + + float const size = m_tool->sizePlug()->getValue(); + Imath::V3f const scale( size, -size, 1.f ); + Imath::Color4f const colourFG = convertToColor4f( m_tool->colourPlug()->getValue() ); + Imath::Color4f const colourHL = convertToColor4f( m_tool->cursorColourPlug()->getValue() ); + + // get cursor raster position + + int cursorVertexId = -1; + Imath::V2f const cursorRasterPos = m_tool->cursorPos(); + Imath::V2f cursorVertexRasterPos = Imath::V2f( -1.f ); + float minDistance2 = std::numeric_limits< float >::max(); + + // get cursor search radius + // + // NOTE : when the cursor position is invalid set the radius to zero to disable search. + + Imath::Box2i const viewport( Imath::V2i( 0 ), viewportGadget->getViewport() ); + float const cursorRadius = ( m_tool->cursorPosValid() && viewport.intersects( cursorRasterPos ) ) + ? m_tool->cursorRadiusPlug()->getValue() : 0.f; + float const cursorRadius2 = cursorRadius * cursorRadius; + + // loop through current selection + + std::stringstream oss; + for( std::vector< CSGafferUI::CsVisualiseVertexIdTool::Selection >::const_iterator + it = m_tool->selection().begin(), + itEnd = m_tool->selection().end(); it != itEnd; ++it ) + { + GafferScene::ScenePlug::PathScope scope( &( ( *it ).context() ), &( ( *it ).path() ) ); + + // check path exists + + if( !( ( *it ).scene().existsPlug()->getValue() ) ) + { + continue; + } + + // extract primitive + + IECoreScene::ConstPrimitivePtr const primitive = + IECore::runTimeCast< IECoreScene::Primitive const >( + ( *it ).scene().objectPlug()->getValue() ); + + if( ! primitive ) + { + continue; + } + + // find "P" vertex attribute + // + // TODO : We need to use the same polygon offset as the Viewer uses when it draws the + // primitive in polygon points mode. For mesh primitives topology may be different, + // primitive variables were converted to face varying and the mesh triangulated + // with vertex positions duplicated. This means that gl_VertexID in the shader + // no longer corresponds to the vertex id we want to display. It also means there + // may be multiple vertices in the IECoreGL mesh for each vertex in the IECore mesh. + // To get the correct polygon offset we need to draw the mesh using the same + // OpenGL draw call as the Viewer used so we must draw the IECoreGL mesh. So + // we need to search for the (posibly multiple) vertices that correspond to each + // original vertex. If any of these IECoreGL mesh vertices are visible we display + // the IECore mesh vertex id. To accelerate the search we build a multi map keyed + // on vertex position. This assumes that the triangulation and/or conversion to + // face varying attributes processing in IECore does not alter the position of the + // vertices. The building of this map is done after we issue the draw call for the + // mesh primitive, this gives OpenGL an opportunity to concurrently execute the + // visibility pass while we are building the map, ready for the map buffer operation. + // For points and curves primitives there is no polygon offset. For all primitives + // there may be a slight slight precision difference in o2c transform so push vertices + // forward. + // NOTE : a cheap alternative approach that solves most of the above problems is to draw + // the visibility pass using "fat" points which cover multiple pixels. This still + // has problems for vertices with negative surrounding curvature ... + // + // NOTE : We use the primitive variable from the IECore primitive as that has + // vertex interpolation. + + IECore::ConstV3fVectorDataPtr const pData = + primitive->expandedVariableData< IECore::V3fVectorData >( + g_pName, IECoreScene::PrimitiveVariable::Vertex, false /* throwIfInvalid */ ); + + if( ! pData ) + { + continue; + } + + // retrieve cached opengl buffer data + + IECoreGL::ConstBufferPtr const pBuffer = + IECore::runTimeCast< IECoreGL::Buffer const >( converter->convert( pData.get() ) ); + + // get the object to world transform + + Imath::M44f o2w; + GafferScene::ScenePlug::ScenePath path( ( *it ).path() ); + while( ! path.empty() ) + { + scope.setPath( & path ); + o2w = o2w * ( *it ).scene().transformPlug()->getValue(); + path.pop_back(); + } + + // compute object to clip matrix + + uniforms.o2c = o2w * w2c; + + // upload opengl uniform block data + + glBindBufferBase( GL_UNIFORM_BUFFER, g_uniformBlockBindingIndex, m_uniformBuffer->m_buffer ); + glBufferData( GL_UNIFORM_BUFFER, sizeof( UniformBlock ), & uniforms, GL_DYNAMIC_DRAW ); + + // ensure storage buffer capacity + + glBindBufferBase( GL_SHADER_STORAGE_BUFFER, g_storageBlockBindingIndex, m_storageBuffer->m_buffer ); + + std::size_t const storageCapacity = + ( pData->readable().size() / static_cast< std::size_t >( 32 ) ) + + static_cast< std::size_t >( 1 ); + std::size_t const storageSize = sizeof( std::uint32_t ) * storageCapacity; + + if( m_storageCapacity < storageCapacity ) + { + glBufferData( GL_SHADER_STORAGE_BUFFER, storageSize, 0, GL_DYNAMIC_DRAW ); + m_storageCapacity = storageCapacity; + } + + // clear storage buffer + // + // NOTE : Shader writes to individual bits using atomicOr instruction so region of + // storage buffer being used for current object needs to be cleared to zero + + GLuint const zeroValue = 0u; + glClearBufferSubData( GL_SHADER_STORAGE_BUFFER, GL_R32UI, 0, storageSize, + GL_RED_INTEGER, GL_UNSIGNED_INT, & zeroValue ); + + // set opengl state + + glPointSize( 3.f ); + glDepthFunc( GL_LEQUAL ); + if( ! depthEnabled ) glEnable( GL_DEPTH_TEST ); + if( depthEnabled ) glDisable( GL_DEPTH_TEST ); + if( depthWriteEnabled ) glDepthMask( GL_FALSE ); + if( multisampleEnabled ) glDisable( GL_MULTISAMPLE ); + + // set opengl vertex attribute array state + + glPushClientAttrib( GL_CLIENT_VERTEX_ARRAY_BIT ); + + glVertexAttribDivisor( ATTRIB_GLSL_LOCATION_PS, 0 ); + glEnableVertexAttribArray( ATTRIB_GLSL_LOCATION_PS ); + + // set visibility pass shader + + glUseProgram( m_shader->program() ); + + // draw points and ouput visibility to storage buffer + + glBindBuffer( GL_ARRAY_BUFFER, pBuffer->m_buffer ); + glVertexAttribPointer( ATTRIB_GLSL_LOCATION_PS, 3, GL_FLOAT, GL_FALSE, 0, + static_cast< void const* >( 0 ) ); + glDrawArrays( GL_POINTS, 0, static_cast< GLsizei >( pData->readable().size() ) ); + + // restore opengl state + + glPopClientAttrib(); + glBindBuffer( GL_ARRAY_BUFFER, arrayBinding ); + glBindBuffer( GL_UNIFORM_BUFFER, uniformBinding ); + + glPointSize( pointSize ); + glDepthFunc( depthFunc ); + if( ! depthEnabled ) glDisable( GL_DEPTH_TEST ); + if( depthEnabled ) glEnable( GL_DEPTH_TEST ); + if( depthWriteEnabled ) glDepthMask( GL_TRUE ); + if( multisampleEnabled ) glEnable( GL_MULTISAMPLE ); + glUseProgram( shaderProgram ); + + // map storage buffer + + std::uint32_t const* const vBuffer = + static_cast< std::uint32_t* >( glMapBufferRange( + GL_SHADER_STORAGE_BUFFER, 0, storageSize, GL_MAP_READ_BIT ) ); + glBindBuffer( GL_SHADER_STORAGE_BUFFER, storageBinding ); + + // draw vertex ids offset to vertex position in raster space + + if( vBuffer ) + { + GafferUI::ViewportGadget::RasterScope raster( viewportGadget ); + + std::vector< Imath::V3f > const& points = pData->readable(); + for( int i = 0; i < points.size(); ++i ) + { + // check visibility of vertex + + std::uint32_t const index = static_cast< std::uint32_t >( i ) / static_cast< std::uint32_t >( 32u ); + std::uint32_t const value = static_cast< std::uint32_t >( i ) % static_cast< std::uint32_t >( 32u ); + + if( vBuffer[ index ] & ( static_cast< std::uint32_t >( 1u ) << value ) ) + { + // transform vertex position to raster space and do manual scissor test + // + // NOTE : visibility pass encorporates scissor test which culls most + // vertices however some will slip through as visibility pass + // draws "fat" points. bounds test is cheap. + + Imath::V3f worldPos; + o2w.multVecMatrix( points[ i ], worldPos ); + Imath::V2f rasterPos = viewportGadget->worldToRasterSpace( worldPos ); + if( rasterBounds.intersects( rasterPos ) ) + { + int vertexId = i; + + // update cursor vertex id + // + // NOTE : We defer drawing of the vertex id currently under the cursor, so + // draw the last vertex id label if we replace the cursor vertex id + + float const distance2 = ( cursorRasterPos - rasterPos ).length2(); + if( ( distance2 < cursorRadius2 ) && ( distance2 < minDistance2 ) ) + { + using std::swap; + swap( cursorVertexId, vertexId ); + swap( cursorVertexRasterPos, rasterPos ); + minDistance2 = distance2; + } + + // draw vertex id label + + if( vertexId != -1 ) + { + oss.str( "" ); + oss.clear(); + oss << vertexId; + std::string const text = oss.str(); + + glPushMatrix(); + glTranslatef( rasterPos.x - style->textBound( GafferUI::Style::LabelText, text ).size().x * 0.5f * scale.x, rasterPos.y, 0.f ); + glScalef( scale.x, scale.y, scale.z ); + style->renderText( GafferUI::Style::LabelText, text, GafferUI::Style::NormalState, & colourFG ); + glPopMatrix(); + } + } + } + } + + // unmap storage buffer + + glBindBuffer( GL_SHADER_STORAGE_BUFFER, m_storageBuffer->m_buffer ); + glUnmapBuffer( GL_SHADER_STORAGE_BUFFER ); + glBindBuffer( GL_SHADER_STORAGE_BUFFER, storageBinding ); + } + + glBindBuffer( GL_SHADER_STORAGE_BUFFER, storageBinding ); + } + + // draw cursor vertex + + if( cursorVertexId != -1 ) + { + GafferUI::ViewportGadget::RasterScope raster( viewportGadget ); + + oss.str( "" ); + oss.clear(); + oss << cursorVertexId; + std::string const text = oss.str(); + + glPushMatrix(); + glTranslatef( cursorVertexRasterPos.x - style->textBound( GafferUI::Style::LabelText, text ).size().x * scale.x, cursorVertexRasterPos.y, 0.f ); + glScalef( scale.x * 2.f, scale.y * 2.f, scale.z ); + style->renderText( GafferUI::Style::LabelText, text, GafferUI::Style::NormalState, & colourHL ); + glPopMatrix(); + } + + // set tool cursor vertex id + + m_tool->cursorVertexId( cursorVertexId ); + } + +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 61 ) + Imath::Box3f renderBound() const override + { + // NOTE : for now just return an infinite box + + Imath::Box3f b; + b.makeInfinite(); + return b; + } +# endif + +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 61 ) + unsigned layerMask() const override + { + return ( m_tool ) + ? static_cast< unsigned >( GafferUI::Gadget::Layer::MidFront ) + : static_cast< unsigned >( 0 ); + } +# else + bool hasLayer( GafferUI::Gadget::Layer layer ) const override + { + return ( m_tool && + ( layer == GafferUI::Gadget::Layer::MidFront ) ); + } +# endif + + private: + + void buildShader() const + { + if( ! m_shader ) + { + m_shader = IECoreGL::ShaderLoader::defaultShaderLoader()->create( + g_vertSource, std::string(), g_fragSource ); + if( m_shader ) + { + GLuint const program = m_shader->program(); + GLuint const uniformblockIndex = glGetProgramResourceIndex( program, GL_UNIFORM_BLOCK, UNIFORM_BLOCK_NAME ); + if( uniformblockIndex != GL_INVALID_INDEX ) + { + glUniformBlockBinding( program, uniformblockIndex, g_uniformBlockBindingIndex ); + } + GLuint const storageblockIndex = glGetProgramResourceIndex( program, GL_SHADER_STORAGE_BLOCK, STORAGE_BLOCK_NAME ); + if( storageblockIndex != GL_INVALID_INDEX ) + { + glShaderStorageBlockBinding( program, storageblockIndex, g_storageBlockBindingIndex ); + } + } + } + } + + CSGafferUI::CsVisualiseVertexIdTool* m_tool; + mutable IECoreGL::ConstShaderPtr m_shader; + mutable IECoreGL::ConstBufferPtr m_uniformBuffer; + mutable IECoreGL::ConstBufferPtr m_storageBuffer; + mutable std::size_t m_storageCapacity; + }; + +} // namespace + +namespace CSGafferUI +{ + GAFFER_NODE_DEFINE_TYPE( CsVisualiseVertexIdTool ); + + GafferUI::Tool::ToolDescription< CsVisualiseVertexIdTool, GafferSceneUI::SceneView > CsVisualiseVertexIdTool::m_toolDescription; + + size_t CsVisualiseVertexIdTool::m_firstPlugIndex = 0; + + CsVisualiseVertexIdTool::CsVisualiseVertexIdTool + ( + GafferSceneUI::SceneView* const view, + std::string const& name + ) + : GafferSceneUI::SelectionTool( view, name ) +# if GAFFER_COMPATIBILITY_VERSION < MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + , m_contextChangedConnection() +# endif + , m_preRenderConnection() + , m_buttonPressConnection() + , m_dragBeginConnection() + , m_gadget( new Gadget( *this ) ) + , m_selection() + , m_cursorPos( -1, -1 ) + , m_cursorPosValid( false ) + , m_cursorValue() + , m_cursorVertexId( -1 ) + , m_gadgetDirty( true ) + , m_selectionDirty( true ) + , m_priorityPathsDirty( true ) + , m_acceptedButtonPress( false ) + , m_initiatedDrag( false ) + { + // add gadget to view and hide + + view->viewportGadget()->addChild( m_gadget ); + m_gadget->setVisible( false ); + + // store offset of first plug + + storeIndexOfNextChild( m_firstPlugIndex ); + + // add child plugs + + addChild( new Gaffer::FloatPlug( "size", Gaffer::Plug::In, g_textSizeDefault, g_textSizeMin ) ); + addChild( new Gaffer::Color3fPlug( "colour", Gaffer::Plug::In, g_colourFGDefault ) ); + addChild( new Gaffer::Color3fPlug( "cursorColour", Gaffer::Plug::In, g_colourHLDefault ) ); + addChild( new Gaffer::FloatPlug( "cursorRadius", Gaffer::Plug::In, g_cursorRadiusDefault, 0.f ) ); + addChild( new GafferScene::ScenePlug( "__scene", Gaffer::Plug::In ) ); + + // connect our internal scene plug to the parent view's scene plug + + internalScenePlug()->setInput( view->inPlug< GafferScene::ScenePlug >() ); + + // connect signal handlers + // + // NOTE : connecting to the viewport gadget means we will get called for all events + // which makes sense for key events, however we do not want to display vertex id + // text when the mouse is over another gadget, (eg. Transform Tool handle) + // so instead connect to scene gadget signal. + // NOTE : There are other handlers that will attempt to consume button and drag + // events so connect handlers at the front of button/drag signal handler queues. + + view->viewportGadget()->keyPressSignal().connect( + boost::bind( & CsVisualiseVertexIdTool::keyPress, this, boost::placeholders::_2 ) ); + + // NOTE : drag end and button release handlers remain whilst tool inactive in case tool + // is made inactive after button pressed or drag initiated in which case these + // handlers still need to tidy up state. + + sceneGadget()->buttonReleaseSignal(). +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 62 ) + connectFront +# else + connect +# endif + ( boost::bind( & CsVisualiseVertexIdTool::buttonRelease, this, boost::placeholders::_2 ) +# if ( GAFFER_MILESTONE_VERSION == 0 && GAFFER_MAJOR_VERSION < 62 ) + , boost::signals::at_front +# endif + ); + + sceneGadget()->dragEndSignal(). +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 62 ) + connectFront +# else + connect +# endif + ( boost::bind( & CsVisualiseVertexIdTool::dragEnd, this, boost::placeholders::_2 ) +# if ( GAFFER_MILESTONE_VERSION == 0 && GAFFER_MAJOR_VERSION < 62 ) + , boost::signals::at_front +# endif + ); + + // NOTE : mouse tracking handlers remain connected whilst tool inactive as they track the cursor + // line and whether its valid or not. This prevents the vertex id display from "sticking" to + // edge of viewport when cursor leaves viewport's screen space. It also means that we do + // not have to work out the cursor line and whether its valid when tool is made active. + + sceneGadget()->enterSignal().connect( + boost::bind( & CsVisualiseVertexIdTool::enter, this, boost::placeholders::_2 ) ); + sceneGadget()->leaveSignal().connect( + boost::bind( & CsVisualiseVertexIdTool::leave, this, boost::placeholders::_2 ) ); + sceneGadget()->mouseMoveSignal().connect( + boost::bind( & CsVisualiseVertexIdTool::mouseMove, this, boost::placeholders::_2 ) ); + + plugDirtiedSignal().connect( + boost::bind( & CsVisualiseVertexIdTool::plugDirtied, this, boost::placeholders::_1 ) ); + +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + view->contextChangedSignal().connect( + boost::bind( & CsVisualiseVertexIdTool::contextChanged, this ) ); + GafferSceneUI::ScriptNodeAlgo::selectedPathsChangedSignal( view->scriptNode() ).connect( + boost::bind( &CsVisualiseVertexIdTool::selectedPathsChanged, this ) ); +# else + connectToViewContext(); + view->contextChangedSignal().connect( + boost::bind( & CsVisualiseVertexIdTool::connectToViewContext, this ) ); +# endif + + Gaffer::Metadata::plugValueChangedSignal().connect( + boost::bind( & CsVisualiseVertexIdTool::metadataChanged, this, boost::placeholders::_3 ) ); + Gaffer::Metadata::nodeValueChangedSignal().connect( + boost::bind( & CsVisualiseVertexIdTool::metadataChanged, this, boost::placeholders::_2 ) ); + } + + CsVisualiseVertexIdTool::~CsVisualiseVertexIdTool() + { + // NOTE : ensure that the gadget's reference to the tool is reset + + static_cast< Gadget* >( m_gadget.get() )->resetTool(); + } + + Gaffer::FloatPlug* CsVisualiseVertexIdTool::sizePlug() + { + return const_cast< Gaffer::FloatPlug* >( + static_cast< CsVisualiseVertexIdTool const* >( this )->sizePlug() ); + } + + Gaffer::FloatPlug const* CsVisualiseVertexIdTool::sizePlug() const + { + return getChild< Gaffer::FloatPlug >( m_firstPlugIndex + 0 ); + } + + Gaffer::Color3fPlug* CsVisualiseVertexIdTool::colourPlug() + { + return const_cast< Gaffer::Color3fPlug* >( + static_cast< CsVisualiseVertexIdTool const* >( this )->colourPlug() ); + } + + Gaffer::Color3fPlug const* CsVisualiseVertexIdTool::colourPlug() const + { + return getChild< Gaffer::Color3fPlug >( m_firstPlugIndex + 1 ); + } + + Gaffer::Color3fPlug* CsVisualiseVertexIdTool::cursorColourPlug() + { + return const_cast< Gaffer::Color3fPlug* >( + static_cast< CsVisualiseVertexIdTool const* >( this )->cursorColourPlug() ); + } + + Gaffer::Color3fPlug const* CsVisualiseVertexIdTool::cursorColourPlug() const + { + return getChild< Gaffer::Color3fPlug >( m_firstPlugIndex + 2 ); + } + + Gaffer::FloatPlug* CsVisualiseVertexIdTool::cursorRadiusPlug() + { + return const_cast< Gaffer::FloatPlug* >( + static_cast< CsVisualiseVertexIdTool const* >( this )->cursorRadiusPlug() ); + } + + Gaffer::FloatPlug const* CsVisualiseVertexIdTool::cursorRadiusPlug() const + { + return getChild< Gaffer::FloatPlug >( m_firstPlugIndex + 3 ); + } + + GafferScene::ScenePlug* CsVisualiseVertexIdTool::internalScenePlug() + { + return const_cast< GafferScene::ScenePlug* >( + static_cast< CsVisualiseVertexIdTool const* >( this )->internalScenePlug() ); + } + + GafferScene::ScenePlug const* CsVisualiseVertexIdTool::internalScenePlug() const + { + return getChild< GafferScene::ScenePlug >( m_firstPlugIndex + 4 ); + } + + std::vector< CsVisualiseVertexIdTool::Selection > const& CsVisualiseVertexIdTool::selection() const + { + return m_selection; + } + + Imath::V2f CsVisualiseVertexIdTool::cursorPos() const + { + return m_cursorPos; + } + + bool CsVisualiseVertexIdTool::cursorPosValid() const + { + return m_cursorPosValid; + } + + void CsVisualiseVertexIdTool::cursorVertexId( int const vertexId ) + { + m_cursorVertexId = vertexId; + } + + void CsVisualiseVertexIdTool::connectOnActive() + { + // NOTE : There are other handlers that will attempt to consume button and drag events + // so connect handlers at the front of button/drag signal handler queues. + + m_buttonPressConnection = sceneGadget()->buttonPressSignal(). +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 62 ) + connectFront +# else + connect +# endif + ( boost::bind( & CsVisualiseVertexIdTool::buttonPress, this, boost::placeholders::_2 ) +# if ( GAFFER_MILESTONE_VERSION == 0 && GAFFER_MAJOR_VERSION < 62 ) + , boost::signals::at_front +# endif + ); + m_dragBeginConnection = sceneGadget()->dragBeginSignal(). +# if ( GAFFER_MILESTONE_VERSION > 0 || GAFFER_MAJOR_VERSION >= 62 ) + connectFront +# else + connect +# endif + ( boost::bind( & CsVisualiseVertexIdTool::dragBegin, this, boost::placeholders::_2 ) +# if ( GAFFER_MILESTONE_VERSION == 0 && GAFFER_MAJOR_VERSION < 62 ) + , boost::signals::at_front +# endif + ); + + m_preRenderConnection = view()->viewportGadget()->preRenderSignal().connect( + boost::bind( & CsVisualiseVertexIdTool::preRender, this ) ); + + // NOTE : redraw necessary to ensure value display updated. + + view()->viewportGadget()->renderRequestSignal()( view()->viewportGadget() ); + } + + void CsVisualiseVertexIdTool::disconnectOnInactive() + { + m_preRenderConnection.disconnect(); + m_buttonPressConnection.disconnect(); + m_dragBeginConnection.disconnect(); + } + +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + void CsVisualiseVertexIdTool::contextChanged() + { + // Context changes can change the scene, which in turn + // dirties our selection. + selectedPathsChanged(); + } + + void CsVisualiseVertexIdTool::selectedPathsChanged() + { + m_selectionDirty = true; + m_gadgetDirty = true; + m_priorityPathsDirty = true; + } +# else + void CsVisualiseVertexIdTool::connectToViewContext() + { + m_contextChangedConnection = view()->getContext()->changedSignal().connect( + boost::bind( & CsVisualiseVertexIdTool::contextChanged, this, boost::placeholders::_2 ) ); + } + + void CsVisualiseVertexIdTool::contextChanged + ( + IECore::InternedString const& name + ) + { + if( GafferSceneUI::ContextAlgo::affectsSelectedPaths( name ) || + GafferSceneUI::ContextAlgo::affectsLastSelectedPath( name ) || + ! boost::starts_with( name.string(), "ui:" ) ) + { + m_selectionDirty = true; + m_gadgetDirty = true; + m_priorityPathsDirty = true; + } + } +# endif + + bool CsVisualiseVertexIdTool::mouseMove + ( + GafferUI::ButtonEvent const& event + ) + { + if( m_initiatedDrag ) + { + return false; + } + + updateCursorPos( event, true ); + + // NOTE : only schedule redraw if tool active + + if( activePlug()->getValue() ) + { + view()->viewportGadget()->renderRequestSignal()( view()->viewportGadget() ); + } + + return false; + } + + void CsVisualiseVertexIdTool::enter + ( + GafferUI::ButtonEvent const& event + ) + { + updateCursorPos( event, true ); + + // NOTE : only schedule redraw if tool active + + if( activePlug()->getValue() ) + { + view()->viewportGadget()->renderRequestSignal()( view()->viewportGadget() ); + } + } + + void CsVisualiseVertexIdTool::leave + ( + GafferUI::ButtonEvent const& event + ) + { + updateCursorPos( event, false ); + + // NOTE : only schedule redraw if tool active + + if( activePlug()->getValue() ) + { + view()->viewportGadget()->renderRequestSignal()( view()->viewportGadget() ); + } + } + + bool CsVisualiseVertexIdTool::keyPress + ( + GafferUI::KeyEvent const& event + ) + { + if( ! activePlug()->getValue() ) + { + return false; + } + + // allow user to scale text with +/- keys + + if( event.key == "Plus" || event.key == "Equal" ) + { + sizePlug()->setValue( sizePlug()->getValue() + g_textSizeInc ); + } + else + if( event.key == "Minus" || event.key == "Underscore" ) + { + sizePlug()->setValue( std::max( sizePlug()->getValue() - g_textSizeInc, g_textSizeMin ) ); + } + + return false; + } + + bool CsVisualiseVertexIdTool::buttonPress + ( + GafferUI::ButtonEvent const& event + ) + { + m_acceptedButtonPress = false; + m_initiatedDrag = false; + + if( ( event.button & GafferUI::ButtonEvent::Left ) ) + { + updateCursorValue(); + if( m_cursorValue ) + { + m_acceptedButtonPress = true; + return true; + } + } + + return false; + } + + bool CsVisualiseVertexIdTool::buttonRelease + ( + GafferUI::ButtonEvent const& event + ) + { + m_acceptedButtonPress = false; + m_initiatedDrag = false; + + return false; + } + + IECore::RunTimeTypedPtr + CsVisualiseVertexIdTool::dragBegin + ( + GafferUI::DragDropEvent const& event + ) + { + m_initiatedDrag = false; + + if( ! m_acceptedButtonPress ) + { + return IECore::RunTimeTypedPtr(); + } + + m_acceptedButtonPress = false; + + if( m_cursorValue ) + { + // NOTE : There is a possibility that the tool has become inactive since the button + // press event that triggered the drag was accepted, the cutoff point is the + // button press event, so any change to the active state after that does not + // affect an ongoing drag operation. We therefore always request a redraw + // here so that the displayed value is cleared. + + m_initiatedDrag = true; + m_cursorPosValid = false; + view()->viewportGadget()->renderRequestSignal()( view()->viewportGadget() ); + GafferUI::Pointer::setCurrent( "values" ); + } + + return m_cursorValue; + } + + bool CsVisualiseVertexIdTool::dragEnd + ( + GafferUI::DragDropEvent const& event + ) + { + if( ! m_initiatedDrag ) + { + return false; + } + + m_initiatedDrag = false; + updateCursorPos( event, true ); + GafferUI::Pointer::setCurrent( "" ); + return true; + } + + void CsVisualiseVertexIdTool::plugDirtied + ( + Gaffer::Plug const* const plug + ) + { + if( ( plug == activePlug() ) || + ( plug == internalScenePlug()->objectPlug() ) || + ( plug == internalScenePlug()->transformPlug() ) ) + { + m_selectionDirty = true; + m_gadgetDirty = true; + m_priorityPathsDirty = true; + } + else + if( ( plug == sizePlug() ) || + ( plug == colourPlug() ) || + ( plug == cursorColourPlug() ) || + ( plug == cursorRadiusPlug() ) ) + { + m_gadgetDirty = true; + view()->viewportGadget()->renderRequestSignal()( view()->viewportGadget() ); + } + + if( plug == activePlug() ) + { + if( activePlug()->getValue() ) + { + connectOnActive(); + } + else + { + disconnectOnInactive(); + m_gadget->setVisible( false ); + + sceneGadget()->setPriorityPaths( IECore::PathMatcher() ); + } + } + } + + void CsVisualiseVertexIdTool::metadataChanged + ( + IECore::InternedString const& key + ) + { + if( ! Gaffer::MetadataAlgo::readOnlyAffectedByChange( key ) ) + { + return; + } + + if( ! m_selectionDirty ) + { + m_selectionDirty = true; + } + + if( ! m_gadgetDirty ) + { + m_gadgetDirty = true; + view()->viewportGadget()->renderRequestSignal()( view()->viewportGadget() ); + } + } + + void CsVisualiseVertexIdTool::updateSelection() const + { + if( ! m_selectionDirty ) + { + return; + } + + m_selection.clear(); + m_selectionDirty = false; + + if( ! activePlug()->getValue() ) + { + return; + } + + GafferScene::ScenePlug const* scene = + internalScenePlug()->getInput< GafferScene::ScenePlug >(); + + if( !( scene ) || + !( scene = scene->getInput< GafferScene::ScenePlug >() ) ) + { + return; + } + +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + IECore::PathMatcher const selectedPaths = + GafferSceneUI::ScriptNodeAlgo::getSelectedPaths( view()->scriptNode() ); +# else + IECore::PathMatcher const selectedPaths = + GafferSceneUI::ContextAlgo::getSelectedPaths( view()->getContext() ); +# endif + + if( selectedPaths.isEmpty() ) + { + return; + } + + for( IECore::PathMatcher::Iterator it = selectedPaths.begin(), + itEnd = selectedPaths.end(); it != itEnd; ++it ) + { +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + m_selection.emplace_back( *( scene ), *it, *( view()->context() ) ); +# else + m_selection.emplace_back( *( scene ), *it, *( view()->getContext() ) ); +# endif + } + } + + void CsVisualiseVertexIdTool::preRender() + { + updateSelection(); + + if( m_priorityPathsDirty ) + { +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + static_cast< GafferSceneUI::SceneGadget* >( view()->viewportGadget()->getPrimaryChild() ) + ->setPriorityPaths( ( m_selection.empty() ) + ? IECore::PathMatcher() + : GafferSceneUI::ScriptNodeAlgo::getSelectedPaths( view()->scriptNode() ) ); +# else + static_cast< GafferSceneUI::SceneGadget* >( view()->viewportGadget()->getPrimaryChild() ) + ->setPriorityPaths( ( m_selection.empty() ) + ? IECore::PathMatcher() + : GafferSceneUI::ContextAlgo::getSelectedPaths( view()->getContext() ) ); +# endif + m_priorityPathsDirty = false; + } + + if( m_selection.empty() ) + { + m_gadget->setVisible( false ); + return; + } + + m_gadget->setVisible( true ); + + if( m_gadgetDirty ) + { + m_gadgetDirty = false; + } + } + + void CsVisualiseVertexIdTool::updateCursorPos + ( + GafferUI::ButtonEvent const& event, + bool const valid + ) + { + // update cursor raster position + // + // NOTE : the cursor position is stored in raster space so it is free of camera + // transformations so we do not need to track camera changes. + + if( valid ) + { + assert( view() ); + assert( view()->viewportGadget() ); + + m_cursorPos = view()->viewportGadget()->gadgetToRasterSpace( event.line.p1, sceneGadget() ); + } + + m_cursorPosValid = valid; + } + + void CsVisualiseVertexIdTool::updateCursorValue() + { + IECore::DataPtr cursorValue = m_cursorValue; + m_cursorValue.reset(); + + // NOTE : cursor value invalid when cursor position is invalid (during drag or no cursor focus) + + if( ! m_cursorPosValid || m_cursorVertexId == -1 ) + { + return; + } + + // store cursor value + + IECore::IntDataPtr data = + IECore::runTimeCast< IECore::IntData >( cursorValue ); + if( ! data ) data.reset( new IECore::IntData() ); + data->writable() = m_cursorVertexId; + cursorValue = data; + + m_cursorValue = cursorValue; + } + + GafferSceneUI::SceneGadget* CsVisualiseVertexIdTool::sceneGadget() + { + return const_cast< GafferSceneUI::SceneGadget* >( + static_cast< CsVisualiseVertexIdTool const* >( this )->sceneGadget() ); + } + + GafferSceneUI::SceneGadget const* CsVisualiseVertexIdTool::sceneGadget() const + { + return IECore::runTimeCast< GafferSceneUI::SceneGadget const >( + view()->viewportGadget()->getPrimaryChild() ); + } + + CsVisualiseVertexIdTool::Selection::Selection + ( + GafferScene::ScenePlug const& scene, + GafferScene::ScenePlug::ScenePath const& path, + Gaffer::Context const& context + ) + : m_scene( & scene ) + , m_path( path ) + , m_context( & context ) + {} + + GafferScene::ScenePlug const& CsVisualiseVertexIdTool::Selection::scene() const + { + return *( m_scene ); + } + + GafferScene::ScenePlug::ScenePath const& CsVisualiseVertexIdTool::Selection::path() const + { + return m_path; + } + + Gaffer::Context const& CsVisualiseVertexIdTool::Selection::context() const + { + return *( m_context ); + } + +} // CSGafferUI diff --git a/contrib/visualisers/CsVisualiseVertexIdTool.h b/contrib/visualisers/CsVisualiseVertexIdTool.h new file mode 100644 index 00000000000..58a6e3009cf --- /dev/null +++ b/contrib/visualisers/CsVisualiseVertexIdTool.h @@ -0,0 +1,223 @@ +////////////////////////////////////////////////////////////////////////// +// +// 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. +// +////////////////////////////////////////////////////////////////////////// + +#ifndef CSGAFFERUI_TOOLS_CSVISUALISEVERTEXIDTOOL_H +#define CSGAFFERUI_TOOLS_CSVISUALISEVERTEXIDTOOL_H + +#include "../../GafferTypeIds.h" + +#include +#include +#include +#include +#include + +#include + +namespace CSGafferUI +{ + /** + * @brief Tool that displays vertex ids of primitives in viewport as text. + */ + struct CsVisualiseVertexIdTool + : public GafferSceneUI::SelectionTool + { + /** + * @brief ctor + * @param view parent view + * @param name name for node + */ + explicit + CsVisualiseVertexIdTool + ( + GafferSceneUI::SceneView* view, + std::string const& name = Gaffer::GraphComponent::defaultName< CsVisualiseVertexIdTool >() + ); + + /** + * @brief dtor + */ + ~CsVisualiseVertexIdTool() override; + + /** + * @name GafferPlugAccessors + * @brief Gaffer plug accessor functions + * @{ + */ + + Gaffer::FloatPlug* sizePlug(); + Gaffer::FloatPlug const* sizePlug() const; + + Gaffer::Color3fPlug* colourPlug(); + Gaffer::Color3fPlug const* colourPlug() const; + + Gaffer::Color3fPlug* cursorColourPlug(); + Gaffer::Color3fPlug const* cursorColourPlug() const; + + Gaffer::FloatPlug* cursorRadiusPlug(); + Gaffer::FloatPlug const* cursorRadiusPlug() const; + + /** + * @} + */ + + GAFFER_NODE_DECLARE_TYPE( + CSGafferUI::CsVisualiseVertexIdTool, + CSInternalTypes::CsVisualiseVertexIdToolTypeId, + GafferSceneUI::SelectionTool ); + + /** + * @brief Class encapsulating a selected scene location + */ + struct Selection + { + /** + * @brief ctor + * @param scene scene + * @param path scene path + * @param context context + */ + Selection + ( + GafferScene::ScenePlug const& scene, + GafferScene::ScenePlug::ScenePath const& path, + Gaffer::Context const& context + ); + + /** + * @brief Get the scene + * @return scene + */ + GafferScene::ScenePlug const& scene() const; + + /** + * @brief Get the scene path + * @return scene path + */ + GafferScene::ScenePlug::ScenePath const& path() const; + + /** + * @brief Get the context + * @return context + */ + Gaffer::Context const& context() const; + + private: + + GafferScene::ConstScenePlugPtr m_scene; + GafferScene::ScenePlug::ScenePath m_path; + Gaffer::ConstContextPtr m_context; + }; + + /** + * @brief Get the current selection + * @return current selection + */ + std::vector< Selection > const& selection() const; + + /** + * @brief Get the cursor position in raster space + * @return cursor position in raster space + */ + Imath::V2f cursorPos() const; + + /** + * @brief Is the cursor position valid? + * @return true if cursor position is valid, otherwise false + */ + bool cursorPosValid() const; + + /** + * @brief Set the cursor vertex id + * @param cursor vertex id + */ + void cursorVertexId( int vertexId ); + + private: + + GafferScene::ScenePlug* internalScenePlug(); + GafferScene::ScenePlug const* internalScenePlug() const; + + void connectOnActive(); + void disconnectOnInactive(); + bool mouseMove( GafferUI::ButtonEvent const& event ); + void enter( GafferUI::ButtonEvent const& event ); + void leave( GafferUI::ButtonEvent const& event ); + bool keyPress( GafferUI::KeyEvent const& event ); + bool buttonPress( GafferUI::ButtonEvent const& event ); + bool buttonRelease( GafferUI::ButtonEvent const& event ); + IECore::RunTimeTypedPtr dragBegin( GafferUI::DragDropEvent const& event ); + bool dragEnd( GafferUI::DragDropEvent const& event ); + void plugDirtied( Gaffer::Plug const* plug ); + void metadataChanged( IECore::InternedString const& key ); + void updateSelection() const; + void preRender(); + void updateCursorPos( GafferUI::ButtonEvent const& event, bool valid ); + void updateCursorValue(); + GafferSceneUI::SceneGadget* sceneGadget(); + GafferSceneUI::SceneGadget const* sceneGadget() const; + +# if GAFFER_COMPATIBILITY_VERSION >= MAKE_GAFFER_COMPATIBILITY_VERSION( 1, 5 ) + void contextChanged(); + void selectedPathsChanged(); +# else + void connectToViewContext(); + void contextChanged( IECore::InternedString const& name ); + Gaffer::Signals::ScopedConnection m_contextChangedConnection; +# endif + Gaffer::Signals::ScopedConnection m_preRenderConnection; + Gaffer::Signals::ScopedConnection m_buttonPressConnection; + Gaffer::Signals::ScopedConnection m_dragBeginConnection; + + GafferUI::GadgetPtr m_gadget; + mutable std::vector< Selection > m_selection; + Imath::V2i m_cursorPos; + bool m_cursorPosValid; + IECore::DataPtr m_cursorValue; + int m_cursorVertexId; + bool m_gadgetDirty; + mutable bool m_selectionDirty; + bool m_priorityPathsDirty; + bool m_acceptedButtonPress; + bool m_initiatedDrag; + + static ToolDescription< CsVisualiseVertexIdTool, GafferSceneUI::SceneView > m_toolDescription; + static size_t m_firstPlugIndex; + }; + +} // CSGafferUI + +#endif // CSGAFFERUI_TOOLS_CSVISUALISEVERTEXIDTOOL_H From 468dd1b20d2cf00babf3c3904344d4dccda8ea5a Mon Sep 17 00:00:00 2001 From: Paul-George Roberts Date: Tue, 12 Nov 2024 10:27:28 -0500 Subject: [PATCH 27/34] Visualisers : Contribute UI --- .../visualisers/CsVisualiseOrientToolUI.py | 191 ++++++++++++++++ contrib/visualisers/CsVisualiseValueToolUI.py | 195 +++++++++++++++++ .../visualisers/CsVisualiseVectorToolUI.py | 203 ++++++++++++++++++ .../visualisers/CsVisualiseVertexIdToolUI.py | 179 +++++++++++++++ 4 files changed, 768 insertions(+) create mode 100644 contrib/visualisers/CsVisualiseOrientToolUI.py create mode 100644 contrib/visualisers/CsVisualiseValueToolUI.py create mode 100644 contrib/visualisers/CsVisualiseVectorToolUI.py create mode 100644 contrib/visualisers/CsVisualiseVertexIdToolUI.py diff --git a/contrib/visualisers/CsVisualiseOrientToolUI.py b/contrib/visualisers/CsVisualiseOrientToolUI.py new file mode 100644 index 00000000000..61c8a0ff2a4 --- /dev/null +++ b/contrib/visualisers/CsVisualiseOrientToolUI.py @@ -0,0 +1,191 @@ +########################################################################## +# +# 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 Gaffer +import GafferUI + +from csgaffer.nodes import CsVisualiseOrientTool + +if CsVisualiseOrientTool is not None: + Gaffer.Metadata.registerNode( + CsVisualiseOrientTool, + "description", + """ + Tool for displaying named primitive variables of type Quatf as coordinate frame. + + Use keys (+/-) to change the scale of the displayed coordinate frame. + """, + "viewer:shortCut", + "O", + "viewer:shouldAutoActivate", + False, + "order", + 1003, + "tool:exclusive", + False, + "layout:activator:activatorFalse", + lambda node: False, + plugs={ + "active": ( + "boolPlugValueWidget:image", + "node_icons/tools/visualise_orient_data.png", + "layout:visibilityActivator", + "activatorFalse", + ), + "name": ( + "description", + """ + Specifies the name of the primitive variable to visualise. The data should + be of type Imath::Quatf. + """, + "layout:index", + 0, + "layout:section", + "Settings", + "label", + "Name", + ), + "scale": ( + "description", + """ + Scale factor applied to the orientation data visualisation. + """, + "layout:index", + 1, + "layout:section", + "Settings", + "label", + "Scale", + ), + "colourX": ( + "description", + """ + Colour applied to the orientation X axis visualisation. + """, + "layout:index", + 2, + "layout:section", + "Settings", + "label", + "Colour X", + ), + "colourY": ( + "description", + """ + Colour applied to the orientation Y axis visualisation. + """, + "layout:index", + 3, + "layout:section", + "Settings", + "label", + "Colour Y", + ), + "colourZ": ( + "description", + """ + Colour applied to the orientation Z axis visualisation. + """, + "layout:index", + 4, + "layout:section", + "Settings", + "label", + "Colour Z", + ), + }, + ) + + class _SettingsNodeUI(GafferUI.NodeUI): + def __init__(self, node, **kw): + self.__mainColumn = GafferUI.ListContainer( + GafferUI.ListContainer.Orientation.Vertical, spacing=4, borderWidth=4 + ) + + GafferUI.NodeUI.__init__(self, node, self.__mainColumn, **kw) + + with self.__mainColumn: + self.__plugLayout = GafferUI.PlugLayout(node, rootSection="Settings") + + def plugValueWidget(self, plug): + hierarchy = [] + while not plug.isSame(self.node()): + hierarchy.insert(0, plug) + plug = plug.parent() + + widget = self.__plugLayout.plugValueWidget(hierarchy[0]) + if widget is None: + return None + + for i in range(1, len(hierarchy)): + widget = widget.childPlugValueWidget(hierarchy[i]) + if widget is None: + return None + + return widget + + def setReadOnly(self, readOnly): + if readOnly == Gaffer.MetadataAlgo.getReadOnly(self.node()): + return + + Gaffer.NodeUI.setReadOnly(self, readOnly) + + self.__plugLayout.setReadOnly(readOnly) + + def __launchToolSettings(node, plugValueWidget): + w = GafferUI.Window(sizeMode=GafferUI.Window.SizeMode.Automatic) + w.setTitle("Tool Settings (%s)" % (CsVisualiseOrientTool.staticTypeName())) + w.setChild(GafferUI.NodeUI.create(node)) + plugValueWidget.ancestor(GafferUI.Window).addChildWindow(w, removeOnClose=True) + w.setVisible(True) + + def __plugPopupMenu(menuDefinition, plugValueWidget): + try: + plug = plugValueWidget.getPlug() + except: + pass + else: + node = plug.node() + if plug.getName() == "active" and isinstance(node, CsVisualiseOrientTool): + import functools + + menuDefinition.append("/Tool Settings Divider", {"divider": True}) + menuDefinition.append( + "/Tool Settings", {"command": functools.partial(__launchToolSettings, node, plugValueWidget)} + ) + + GafferUI.NodeUI.registerNodeUI(CsVisualiseOrientTool, _SettingsNodeUI) + GafferUI.PlugValueWidget.popupMenuSignal().connect(__plugPopupMenu, scoped=False) diff --git a/contrib/visualisers/CsVisualiseValueToolUI.py b/contrib/visualisers/CsVisualiseValueToolUI.py new file mode 100644 index 00000000000..e289dc37d30 --- /dev/null +++ b/contrib/visualisers/CsVisualiseValueToolUI.py @@ -0,0 +1,195 @@ +########################################################################## +# +# 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 Gaffer +import GafferUI + +from csgaffer.nodes import CsVisualiseValueTool + +if CsVisualiseValueTool is not None: + Gaffer.Metadata.registerNode( + CsVisualiseValueTool, + "description", + """ + Tool for displaying named primitive variables of type float, V2f or V3f as a coloured overlay. + """, + "viewer:shortCut", + "S", + "viewer:shouldAutoActivate", + False, + "order", + 1001, + "tool:exclusive", + False, + "layout:activator:activatorFalse", + lambda node: False, + plugs={ + "active": ( + "boolPlugValueWidget:image", + "node_icons/tools/visualise_value_data.png", + "layout:visibilityActivator", + "activatorFalse", + ), + "name": ( + "description", + """ + Specifies the name of the primitive variable to visualise. The data should + be of type float, V2f or V3f. + """, + "layout:index", + 0, + "layout:section", + "Settings", + "label", + "Name", + ), + "valueMin": ( + "description", + """ + The minimum data channel value that will be mapped to 0. + + For float data only the first channel is used. For V2f data only the first + and second channels are used. For V3f data all three channels are used. + """, + "layout:index", + 1, + "layout:section", + "Settings", + "label", + "Min Value", + ), + "valueMax": ( + "description", + """ + The maximum data channel value that will be mapped to 1. + + For float data only the first channel is used. For V2f data only the first + and second channels are used. For V3f data all three channels are used. + """, + "layout:index", + 2, + "layout:section", + "Settings", + "label", + "Max Value", + ), + "size": ( + "description", + """ + Specifies the size of the displayed text. + """, + "layout:index", + 3, + "layout:section", + "Settings", + "label", + "Size", + ), + "colour": ( + "description", + """ + Specifies the colour of the displayed text. + """, + "layout:index", + 4, + "layout:section", + "Settings", + "label", + "Colour", + ), + }, + ) + + class _SettingsNodeUI(GafferUI.NodeUI): + def __init__(self, node, **kw): + self.__mainColumn = GafferUI.ListContainer( + GafferUI.ListContainer.Orientation.Vertical, spacing=4, borderWidth=4 + ) + + GafferUI.NodeUI.__init__(self, node, self.__mainColumn, **kw) + + with self.__mainColumn: + self.__plugLayout = GafferUI.PlugLayout(node, rootSection="Settings") + + def plugValueWidget(self, plug): + hierarchy = [] + while not plug.isSame(self.node()): + hierarchy.insert(0, plug) + plug = plug.parent() + + widget = self.__plugLayout.plugValueWidget(hierarchy[0]) + if widget is None: + return None + + for i in range(1, len(hierarchy)): + widget = widget.childPlugValueWidget(hierarchy[i]) + if widget is None: + return None + + return widget + + def setReadOnly(self, readOnly): + if readOnly == Gaffer.MetadataAlgo.getReadOnly(self.node()): + return + + Gaffer.NodeUI.setReadOnly(self, readOnly) + + self.__plugLayout.setReadOnly(readOnly) + + def __launchToolSettings(node, plugValueWidget): + w = GafferUI.Window(sizeMode=GafferUI.Window.SizeMode.Automatic) + w.setTitle("Tool Settings (%s)" % (CsVisualiseValueTool.staticTypeName())) + w.setChild(GafferUI.NodeUI.create(node)) + plugValueWidget.ancestor(GafferUI.Window).addChildWindow(w, removeOnClose=True) + w.setVisible(True) + + def __plugPopupMenu(menuDefinition, plugValueWidget): + try: + plug = plugValueWidget.getPlug() + except: + pass + else: + node = plug.node() + if plug.getName() == "active" and isinstance(node, CsVisualiseValueTool): + import functools + + menuDefinition.append("/Tool Settings Divider", {"divider": True}) + menuDefinition.append( + "/Tool Settings", {"command": functools.partial(__launchToolSettings, node, plugValueWidget)} + ) + + GafferUI.NodeUI.registerNodeUI(CsVisualiseValueTool, _SettingsNodeUI) + GafferUI.PlugValueWidget.popupMenuSignal().connect(__plugPopupMenu, scoped=False) diff --git a/contrib/visualisers/CsVisualiseVectorToolUI.py b/contrib/visualisers/CsVisualiseVectorToolUI.py new file mode 100644 index 00000000000..027c4488c7d --- /dev/null +++ b/contrib/visualisers/CsVisualiseVectorToolUI.py @@ -0,0 +1,203 @@ +########################################################################## +# +# 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 Gaffer +import GafferUI + +from csgaffer.nodes import CsVisualiseVectorTool + +if CsVisualiseVectorTool is not None: + Gaffer.Metadata.registerNode( + CsVisualiseVectorTool, + "description", + """ + Tool for displaying named primitive variables of type V3f as line vectors. + + Use keys (+/-) to change the scale of the displayed line vectors. + """, + "viewer:shortCut", + "V", + "viewer:shouldAutoActivate", + False, + "order", + 1002, + "tool:exclusive", + False, + "layout:activator:activatorFalse", + lambda node: False, + "layout:activator:activatorScale", + lambda node: node["format"].getValue() != CsVisualiseVectorTool.Format.Point, + plugs={ + "active": ( + "boolPlugValueWidget:image", + "node_icons/tools/visualise_vector_data.png", + "layout:visibilityActivator", + "activatorFalse", + ), + "name": ( + "description", + """ + Specifies the name of the primitive variable to visualise. The data should + be of type Imath::V3f. + """, + "layout:index", + 0, + "layout:section", + "Settings", + "label", + "Name", + ), + "format": ( + "description", + """ + Format of data : + + Point - Interpret data as points. + + Vector - Interpret data as vectors. + + Bivector - Interpret data as bivectors (e.g. surface normals) so they remain + orthogonal to the plane containing the vectors whose cross product + they are the result of, under all affine transformations. + """, + "preset:Point", + CsVisualiseVectorTool.Format.Point, + "preset:Vector", + CsVisualiseVectorTool.Format.Vector, + "preset:Bivector", + CsVisualiseVectorTool.Format.Bivector, + "plugValueWidget:type", + "GafferUI.PresetsPlugValueWidget", + "layout:index", + 0, + "layout:accessory", + True, + "layout:width", + 100, + "layout:section", + "Settings", + "label", + "Format", + ), + "scale": ( + "description", + """ + Scale factor applied to the vector data visualisation. + """, + "layout:index", + 1, + "layout:section", + "Settings", + "layout:activator", + "activatorScale", + "label", + "Scale", + ), + "colour": ( + "description", + """ + Colour applied to the vector data visualisation. + """, + "layout:index", + 2, + "layout:section", + "Settings", + "label", + "Colour", + ), + }, + ) + + class _SettingsNodeUI(GafferUI.NodeUI): + def __init__(self, node, **kw): + self.__mainColumn = GafferUI.ListContainer( + GafferUI.ListContainer.Orientation.Vertical, spacing=4, borderWidth=4 + ) + + GafferUI.NodeUI.__init__(self, node, self.__mainColumn, **kw) + + with self.__mainColumn: + self.__plugLayout = GafferUI.PlugLayout(node, rootSection="Settings") + + def plugValueWidget(self, plug): + hierarchy = [] + while not plug.isSame(self.node()): + hierarchy.insert(0, plug) + plug = plug.parent() + + widget = self.__plugLayout.plugValueWidget(hierarchy[0]) + if widget is None: + return None + + for i in range(1, len(hierarchy)): + widget = widget.childPlugValueWidget(hierarchy[i]) + if widget is None: + return None + + return widget + + def setReadOnly(self, readOnly): + if readOnly == Gaffer.MetadataAlgo.getReadOnly(self.node()): + return + + Gaffer.NodeUI.setReadOnly(self, readOnly) + + self.__plugLayout.setReadOnly(readOnly) + + def __launchToolSettings(node, plugValueWidget): + w = GafferUI.Window(sizeMode=GafferUI.Window.SizeMode.Automatic) + w.setTitle("Tool Settings (%s)" % (CsVisualiseVectorTool.staticTypeName())) + w.setChild(GafferUI.NodeUI.create(node)) + plugValueWidget.ancestor(GafferUI.Window).addChildWindow(w, removeOnClose=True) + w.setVisible(True) + + def __plugPopupMenu(menuDefinition, plugValueWidget): + try: + plug = plugValueWidget.getPlug() + except: + pass + else: + node = plug.node() + if plug.getName() == "active" and isinstance(node, CsVisualiseVectorTool): + import functools + + menuDefinition.append("/Tool Settings Divider", {"divider": True}) + menuDefinition.append( + "/Tool Settings", {"command": functools.partial(__launchToolSettings, node, plugValueWidget)} + ) + + GafferUI.NodeUI.registerNodeUI(CsVisualiseVectorTool, _SettingsNodeUI) + GafferUI.PlugValueWidget.popupMenuSignal().connect(__plugPopupMenu, scoped=False) diff --git a/contrib/visualisers/CsVisualiseVertexIdToolUI.py b/contrib/visualisers/CsVisualiseVertexIdToolUI.py new file mode 100644 index 00000000000..7783882f99f --- /dev/null +++ b/contrib/visualisers/CsVisualiseVertexIdToolUI.py @@ -0,0 +1,179 @@ +########################################################################## +# +# 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 Gaffer +import GafferUI + +from csgaffer.nodes import CsVisualiseVertexIdTool + +if CsVisualiseVertexIdTool is not None: + Gaffer.Metadata.registerNode( + CsVisualiseVertexIdTool, + "description", + """ + Tool for displaying the vertex ids of a primitive with a "P" primitive variable. + + Use keys (+/-) to change the size of the displayed text. + """, + "viewer:shortCut", + "U", + "viewer:shouldAutoActivate", + False, + "order", + 1004, + "tool:exclusive", + False, + "layout:activator:activatorFalse", + lambda node: False, + plugs={ + "active": ( + "boolPlugValueWidget:image", + "node_icons/tools/visualise_vertex_ids.png", + "layout:visibilityActivator", + "activatorFalse", + ), + "size": ( + "description", + """ + Specifies the size of the displayed text labels. + """, + "layout:index", + 0, + "layout:section", + "Settings", + "label", + "Text Size", + ), + "colour": ( + "description", + """ + Specifies the colour of the displayed text labels. + """, + "layout:index", + 1, + "layout:section", + "Settings", + "label", + "Text Colour", + ), + "cursorColour": ( + "description", + """ + Specifies the colour of the displayed cursor text label. + """, + "layout:index", + 2, + "layout:section", + "Settings", + "label", + "Cursor Text Colour", + ), + "cursorRadius": ( + "description", + """ + Specifies the search radius distance used to find the nearest vertex id to the cursor. + Set to zero to disable cursor vertex id search. + """, + "layout:index", + 3, + "layout:section", + "Settings", + "label", + "Cursor Search Radius", + ), + }, + ) + + class _SettingsNodeUI(GafferUI.NodeUI): + def __init__(self, node, **kw): + self.__mainColumn = GafferUI.ListContainer( + GafferUI.ListContainer.Orientation.Vertical, spacing=4, borderWidth=4 + ) + + GafferUI.NodeUI.__init__(self, node, self.__mainColumn, **kw) + + with self.__mainColumn: + self.__plugLayout = GafferUI.PlugLayout(node, rootSection="Settings") + + def plugValueWidget(self, plug): + hierarchy = [] + while not plug.isSame(self.node()): + hierarchy.insert(0, plug) + plug = plug.parent() + + widget = self.__plugLayout.plugValueWidget(hierarchy[0]) + if widget is None: + return None + + for i in range(1, len(hierarchy)): + widget = widget.childPlugValueWidget(hierarchy[i]) + if widget is None: + return None + + return widget + + def setReadOnly(self, readOnly): + if readOnly == Gaffer.MetadataAlgo.getReadOnly(self.node()): + return + + Gaffer.NodeUI.setReadOnly(readOnly) + + self.__plugLayout.setReadOnly(readOnly) + + def __launchToolSettings(node, plugValueWidget): + w = GafferUI.Window(sizeMode=GafferUI.Window.SizeMode.Automatic) + w.setTitle("Tool Settings (%s)" % (CsVisualiseVertexIdTool.staticTypeName())) + w.setChild(GafferUI.NodeUI.create(node)) + plugValueWidget.ancestor(GafferUI.Window).addChildWindow(w, removeOnClose=True) + w.setVisible(True) + + def __plugPopupMenu(menuDefinition, plugValueWidget): + try: + plug = plugValueWidget.getPlug() + except: + pass + else: + node = plug.node() + if plug.getName() == "active" and isinstance(node, CsVisualiseVertexIdTool): + import functools + + menuDefinition.append("/Tool Settings Divider", {"divider": True}) + menuDefinition.append( + "/Tool Settings", {"command": functools.partial(__launchToolSettings, node, plugValueWidget)} + ) + + GafferUI.NodeUI.registerNodeUI(CsVisualiseVertexIdTool, _SettingsNodeUI) + GafferUI.PlugValueWidget.popupMenuSignal().connect(__plugPopupMenu, scoped=False) From 8f0d8fc117274284d226cd28015ed4bba4facc85 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Wed, 13 Nov 2024 13:06:12 +0000 Subject: [PATCH 28/34] DisplayTransformTest : Don't rely on `$GAFFER_ROOT/openColorIO` This doesn't exist at Image Engine because they have a custom build layout. Instead just use an alternative config that we already use in other test cases. --- python/GafferImageTest/DisplayTransformTest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/GafferImageTest/DisplayTransformTest.py b/python/GafferImageTest/DisplayTransformTest.py index 59077b7df08..a1a31eaceaa 100644 --- a/python/GafferImageTest/DisplayTransformTest.py +++ b/python/GafferImageTest/DisplayTransformTest.py @@ -249,7 +249,7 @@ def testDisplayAndViewDefaultToConfig( self ) : # Test alternative config - configPath = Gaffer.rootPath() / "openColorIO" / "config.ocio" + configPath = self.openColorIOPath() / "context.ocio" config = PyOpenColorIO.Config.CreateFromFile( str( configPath ) ) explicitDisplayTransform["display"].setValue( config.getDefaultDisplay() ) @@ -257,6 +257,8 @@ def testDisplayAndViewDefaultToConfig( self ) : with Gaffer.Context() as context : GafferImage.OpenColorIOAlgo.setConfig( context, configPath.as_posix() ) + GafferImage.OpenColorIOAlgo.addVariable( context, "CDL", "rec709.spi1d" ) + GafferImage.OpenColorIOAlgo.addVariable( context, "LUT", "cineon.spi1d" ) self.assertImagesEqual( defaultDisplayTransform["out"], explicitDisplayTransform["out"] ) if __name__ == "__main__": From 36fbd9f156f017055bf7c716818d6748445b67db Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Wed, 23 Oct 2024 13:17:35 -0400 Subject: [PATCH 29/34] ColorChooser : Static color slider widgets --- Changes.md | 1 + python/GafferUI/ColorChooser.py | 56 ++++++++++++++++++- .../GafferUI/ColorChooserPlugValueWidget.py | 16 +++++- python/GafferUI/ColorSwatchPlugValueWidget.py | 11 ++++ python/GafferUITest/ColorChooserTest.py | 25 +++++++++ 5 files changed, 105 insertions(+), 4 deletions(-) diff --git a/Changes.md b/Changes.md index 1650bd6d098..a135f493d08 100644 --- a/Changes.md +++ b/Changes.md @@ -10,6 +10,7 @@ Improvements - DeletePoints : Added modes for deleting points based on a list of ids. - Light Editor, Attribute Editor, Spreadsheet : Add original and current color swatches to color popups. - SceneView : Added fallback framing extents to create a reasonable view when `SceneGadget` is empty, for example if the grid is hidden. +- ColorChooser : Added an option to toggle the dynamic update of colors displayed in the slider backgrounds. When enabled, the widget backgrounds update to show the color that will result from moving the indicator to a given position. When disabled, a static range of values is displayed instead. Fixes ----- diff --git a/python/GafferUI/ColorChooser.py b/python/GafferUI/ColorChooser.py index a4ff0e0f10a..d69c7c0f0fa 100644 --- a/python/GafferUI/ColorChooser.py +++ b/python/GafferUI/ColorChooser.py @@ -117,7 +117,7 @@ def _drawIndicator( painter, position ) : # A custom slider for drawing the backgrounds. class _ComponentSlider( GafferUI.Slider ) : - def __init__( self, color, component, **kw ) : + def __init__( self, color, component, dynamicBackground = True, **kw ) : GafferUI.Slider.__init__( self, 0.0, @@ -128,6 +128,7 @@ def __init__( self, color, component, **kw ) : self.color = color self.component = component + self.__dynamicBackground = dynamicBackground # Sets the slider color in RGB space for RGBA channels, # HSV space for HSV channels and TMI space for TMI channels. @@ -140,6 +141,15 @@ def getColor( self ) : return self.color + def setDynamicBackground( self, dynamicBackground ) : + + self.__dynamicBackground = dynamicBackground + self._qtWidget().update() + + def getDynamicBackground( self ) : + + return self.__dynamicBackground + def _drawBackground( self, painter ) : size = self.size() @@ -151,8 +161,17 @@ def _drawBackground( self, painter ) : c1 = imath.Color3f( 0 ) c2 = imath.Color3f( 1 ) else : - c1 = imath.Color3f( self.color[0], self.color[1], self.color[2] ) - c2 = imath.Color3f( self.color[0], self.color[1], self.color[2] ) + if self.__dynamicBackground : + c1 = imath.Color3f( self.color[0], self.color[1], self.color[2] ) + elif self.component in "rgbvi" : + c1 = imath.Color3f( 0 ) + elif self.component == "h" : + c1 = imath.Color3f( 0, 1, 1 ) + elif self.component == "s" : + c1 = imath.Color3f( self.color[0], 0, 1 ) + elif self.component in "tm" : + c1 = imath.Color3f( 0, 0, 0.5 ) + c2 = imath.Color3f( c1 ) a = { "r" : 0, "g" : 1, "b" : 2, "h" : 0, "s" : 1, "v": 2, "t" : 0, "m" : 1, "i" : 2 }[self.component] c1[a] = -1 if self.component in "tm" else 0 c2[a] = 1 @@ -835,6 +854,7 @@ def __init__( self, color=imath.Color3f( 1 ), **kw ) : self.__visibleComponentsChangedSignal = Gaffer.Signals.Signal1() self.__staticComponentChangedSignal = Gaffer.Signals.Signal1() self.__colorFieldVisibleChangedSignal = Gaffer.Signals.Signal1() + self.__dynamicSliderBackgroundsChangedSignal = Gaffer.Signals.Signal1() self.__optionsMenuSignal = Gaffer.Signals.Signal2() self.__colorFieldPrimaryIcon = GafferUI.Image( "colorFieldPrimaryIcon.png" ) @@ -920,6 +940,17 @@ def getColorFieldVisible( self ) : return self.__colorField.getVisible() + def setDynamicSliderBackgrounds( self, dynamic ) : + + for component, slider in self.__sliders.items() : + slider.setDynamicBackground( dynamic ) + + self.__dynamicSliderBackgroundsChangedSignal( self ) + + def getDynamicSliderBackgrounds( self ) : + + return self.__sliders["r"].getDynamicBackground() + ## A signal emitted whenever the color is changed. Slots should # have the signature slot( ColorChooser, reason ). The reason # argument may be passed either a ColorChooser.ColorChangedReason, @@ -952,6 +983,12 @@ def colorFieldVisibleChangedSignal( self ) : return self.__colorFieldVisibleChangedSignal + ## A signal emitted whenever the dynamic colors option is changed. + # Slots should have the signature slot( ColorChooser ). + def dynamicSliderBackgroundsChangedSignal( self ) : + + return self.__dynamicSliderBackgroundsChangedSignal + ## A signal emitted whenever the options menu is opened. # Slots should have the signature slot( ColorChooser, menuDefinition ) # and add menu items to `menuDefinition`. @@ -1018,6 +1055,19 @@ def __optionsMenuDefinition( self ) : } ) + result.append( "/__sliders__", { "divider": True, "label": "Sliders" } ) + + result.append( + "/Dynamic Backgrounds", + { + "command": Gaffer.WeakMethod( self.setDynamicSliderBackgrounds ), + "checkBox": self.getDynamicSliderBackgrounds(), + "description": """With Dynamic Backgrounds enabled, the backgrounds of the color sliders will +update to show the color that will result from moving the indicator to a given +position. When disabled, a static range of values is displayed.""" + } + ) + self.__optionsMenuSignal( self, result ) return result diff --git a/python/GafferUI/ColorChooserPlugValueWidget.py b/python/GafferUI/ColorChooserPlugValueWidget.py index a7e2a2fffc3..9640e4b3ecd 100644 --- a/python/GafferUI/ColorChooserPlugValueWidget.py +++ b/python/GafferUI/ColorChooserPlugValueWidget.py @@ -65,6 +65,10 @@ def __init__( self, plugs, **kw ) : if colorFieldVisible is not None : self.__colorChooser.setColorFieldVisible( colorFieldVisible ) + dynamicSliderBackgrounds = self.__colorChooserOption( "dynamicSliderBackgrounds" ) + if dynamicSliderBackgrounds is not None : + self.__colorChooser.setDynamicSliderBackgrounds( dynamicSliderBackgrounds ) + self.__colorChangedConnection = self.__colorChooser.colorChangedSignal().connect( Gaffer.WeakMethod( self.__colorChanged ) ) @@ -78,6 +82,9 @@ def __init__( self, plugs, **kw ) : self.__colorChooser.colorFieldVisibleChangedSignal().connect( functools.partial( Gaffer.WeakMethod( self.__colorChooserColorFieldVisibleChanged ) ) ) + self.__colorChooser.dynamicSliderBackgroundsChangedSignal().connect( + functools.partial( Gaffer.WeakMethod( self.__dynamicSliderBackgroundsChanged ) ) + ) self.__colorChooser.optionsMenuSignal().connect( functools.partial( Gaffer.WeakMethod( self.__colorChooserOptionsMenu ) ), scoped = False @@ -157,6 +164,10 @@ def __colorChooserColorFieldVisibleChanged( self, colorChooser ) : self.__colorChooserOptionChanged( "colorFieldVisible", colorChooser.getColorFieldVisible() ) + def __dynamicSliderBackgroundsChanged( self, colorChooser ) : + + self.__colorChooserOptionChanged( "dynamicSliderBackgrounds", colorChooser.getDynamicSliderBackgrounds() ) + def __colorChooserOptionsMenu( self, colorChooser, menuDefinition ) : menuDefinition.append( "/__saveDefaultOptions__", { "divider": True, "label": "Defaults" } ) @@ -193,14 +204,16 @@ def saveDefaultOptions( colorChooser, keyPrefix, scriptPath = None ) : visibleComponents = colorChooser.getVisibleComponents() staticComponent = colorChooser.getColorFieldStaticComponent() colorFieldVisible = colorChooser.getColorFieldVisible() + dynamicSliderBackgrounds = colorChooser.getDynamicSliderBackgrounds() for p in [ Gaffer.Color3fPlug, Gaffer.Color4fPlug ] : - for k in [ "visibleComponents", "staticComponent", "colorFieldVisible" ] : + for k in [ "visibleComponents", "staticComponent", "colorFieldVisible", "dynamicSliderBackgrounds" ] : Gaffer.Metadata.deregisterValue( p, keyPrefix + k ) Gaffer.Metadata.registerValue( p, keyPrefix + "visibleComponents", visibleComponents ) Gaffer.Metadata.registerValue( p, keyPrefix + "staticComponent", staticComponent ) Gaffer.Metadata.registerValue( p, keyPrefix + "colorFieldVisible", colorFieldVisible ) + Gaffer.Metadata.registerValue( p, keyPrefix + "dynamicSliderBackgrounds", dynamicSliderBackgrounds ) if scriptPath is None : return @@ -226,6 +239,7 @@ def saveDefaultOptions( colorChooser, keyPrefix, scriptPath = None ) : newScript.append( f"Gaffer.Metadata.registerValue( Gaffer.Color{c}fPlug, \"{keyPrefix}visibleComponents\", \"{visibleComponents}\" )\n" ) newScript.append( f"Gaffer.Metadata.registerValue( Gaffer.Color{c}fPlug, \"{keyPrefix}staticComponent\", \"{staticComponent}\" )\n" ) newScript.append( f"Gaffer.Metadata.registerValue( Gaffer.Color{c}fPlug, \"{keyPrefix}colorFieldVisible\", {colorFieldVisible} )\n" ) + newScript.append( f"Gaffer.Metadata.registerValue( Gaffer.Color{c}fPlug, \"{keyPrefix}dynamicSliderBackgrounds\", {dynamicSliderBackgrounds} )\n" ) with open( scriptPath, "w" ) as outFile : outFile.writelines( newScript ) \ No newline at end of file diff --git a/python/GafferUI/ColorSwatchPlugValueWidget.py b/python/GafferUI/ColorSwatchPlugValueWidget.py index 9c2fdec1fd4..0ed0604bf55 100644 --- a/python/GafferUI/ColorSwatchPlugValueWidget.py +++ b/python/GafferUI/ColorSwatchPlugValueWidget.py @@ -147,6 +147,9 @@ def __init__( self, plugs, parentWindow ) : self.colorChooser().colorFieldVisibleChangedSignal().connect( functools.partial( Gaffer.WeakMethod( self.__colorChooserColorFieldVisibleChanged ) ) ) + self.colorChooser().dynamicSliderBackgroundsChangedSignal().connect( + functools.partial( Gaffer.WeakMethod( self.__dynamicSliderBackgroundsChanged ) ) + ) self.colorChooser().optionsMenuSignal().connect( functools.partial( Gaffer.WeakMethod( self.__colorChooserOptionsMenu ) ), scoped = False @@ -183,6 +186,10 @@ def __init__( self, plugs, parentWindow ) : if colorFieldVisible is not None : self.colorChooser().setColorFieldVisible( colorFieldVisible ) + dynamicSliderBackgrounds = self.__colorChooserOption( "dynamicSliderBackgrounds" ) + if dynamicSliderBackgrounds is not None : + self.colorChooser().setDynamicSliderBackgrounds( dynamicSliderBackgrounds ) + parentWindow.addChildWindow( self, removeOnClose = True ) @classmethod @@ -287,3 +294,7 @@ def __colorChooserStaticComponentChanged( self, colorChooser ) : def __colorChooserColorFieldVisibleChanged( self, colorChooser ) : self.__colorChooserOptionChanged( "colorFieldVisible", colorChooser.getColorFieldVisible() ) + + def __dynamicSliderBackgroundsChanged( self, colorChooser ) : + + self.__colorChooserOptionChanged( "dynamicSliderBackgrounds", colorChooser.getDynamicSliderBackgrounds() ) diff --git a/python/GafferUITest/ColorChooserTest.py b/python/GafferUITest/ColorChooserTest.py index f7a50f0ce85..6c28a62dd4a 100644 --- a/python/GafferUITest/ColorChooserTest.py +++ b/python/GafferUITest/ColorChooserTest.py @@ -123,6 +123,16 @@ def __getColorFieldVisibility( self, widget ) : c = self.__colorChooserFromWidget( widget ) return c.getColorFieldVisible() + def __setDynamicSliderBackgrounds( self, widget, dynamic ) : + + c = self.__colorChooserFromWidget( widget ) + c.setDynamicSliderBackgrounds( dynamic ) + + def __getDynamicSliderBackgrounds( self, widget ) : + + c = self.__colorChooserFromWidget( widget ) + return c.getDynamicSliderBackgrounds() + def testMetadata( self ) : script = Gaffer.ScriptNode() @@ -145,12 +155,14 @@ def testMetadata( self ) : self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:visibleComponents" ) ) self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:staticComponent" ) ) self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:colorFieldVisible" ) ) + self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:dynamicSliderBackgrounds" ) ) # Modify widget self.__setVisibleComponents( widget, "rgbtmi" ) self.__setStaticComponent( widget, "t" ) self.__setColorFieldVisibility( widget, False ) + self.__setDynamicSliderBackgrounds( widget, False ) for c in "rgbtmi" : self.assertTrue( self.__sliderFromWidget( widget, c ).getVisible() ) @@ -158,15 +170,18 @@ def testMetadata( self ) : self.assertFalse( self.__sliderFromWidget( widget, c ).getVisible() ) self.assertEqual( self.__getStaticComponent( widget ), "t" ) self.assertFalse( self.__getColorFieldVisibility( widget ) ) + self.assertFalse( self.__getDynamicSliderBackgrounds( widget ) ) for p in [ "rgbPlug2" ] : self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:visibleComponents" ) ) self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:staticComponent" ) ) self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:colorFieldVisible" ) ) + self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:dynamicSliderBackgrounds" ) ) self.assertEqual( set( Gaffer.Metadata.value( script["node"]["rgbPlug1"], "colorChooser:inline:visibleComponents" ) ), set( "rgbtmi" ) ) self.assertEqual( Gaffer.Metadata.value( script["node"]["rgbPlug1"], "colorChooser:inline:staticComponent" ), "t" ) self.assertFalse( Gaffer.Metadata.value( script["node"]["rgbPlug1"], "colorChooser:inline:colorFieldVisible" ) ) + self.assertFalse( Gaffer.Metadata.value( script["node"]["rgbPlug1"], "colorChooser:inline:dynamicSliderBackgrounds" ) ) # Recreate widget and should have the same state @@ -180,6 +195,7 @@ def testMetadata( self ) : self.assertFalse( self.__sliderFromWidget( widget, c ).getVisible() ) self.assertEqual( self.__getStaticComponent( widget ), "t" ) self.assertFalse( self.__getColorFieldVisibility( widget ) ) + self.assertFalse( self.__getDynamicSliderBackgrounds( widget ) ) # We haven't saved the defaults, so a widget for a second plug # gets the original defaults. @@ -191,11 +207,13 @@ def testMetadata( self ) : self.assertTrue( self.__sliderFromWidget( widget2, c ).getVisible() ) self.assertEqual( self.__getStaticComponent( widget2 ), "v" ) self.assertTrue( self.__getColorFieldVisibility( widget2 ) ) + self.assertTrue( self.__getDynamicSliderBackgrounds( widget2 ) ) for p in [ "rgbPlug2" ] : self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:visibleComponents" ) ) self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:staticComponent" ) ) self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:colorFieldVisible" ) ) + self.assertIsNone( Gaffer.Metadata.value( script["node"][p], "colorChooser:inline:dynamicSliderBackgrounds" ) ) # Don't serialize state @@ -211,11 +229,13 @@ def testMetadata( self ) : self.assertTrue( self.__sliderFromWidget( widget, c ).getVisible() ) self.assertEqual( self.__getStaticComponent( widget ), "v" ) self.assertTrue( self.__getColorFieldVisibility( widget ) ) + self.assertTrue( self.__getDynamicSliderBackgrounds( widget ) ) for p in [ "rgbPlug1", "rgbPlug2" ] : self.assertIsNone( Gaffer.Metadata.value( script2["node"][p], "colorChooser:inline:visibleComponents" ) ) self.assertIsNone( Gaffer.Metadata.value( script2["node"][p], "colorChooser:inline:staticComponent" ) ) self.assertIsNone( Gaffer.Metadata.value( script2["node"][p], "colorChooser:inline:colorFieldVisible" ) ) + self.assertIsNone( Gaffer.Metadata.value( script2["node"][p], "colorChooser:inline:dynamicSliderBackgrounds" ) ) def testSaveDefaultOptions( self ) : @@ -243,12 +263,15 @@ def testSaveDefaultOptions( self ) : self.assertEqual( self.__getStaticComponent( rgbaWidget ), "v" ) self.assertTrue( self.__getColorFieldVisibility( rgbWidget ) ) self.assertTrue( self.__getColorFieldVisibility( rgbaWidget ) ) + self.assertTrue( self.__getDynamicSliderBackgrounds( rgbWidget ) ) + self.assertTrue( self.__getDynamicSliderBackgrounds( rgbaWidget ) ) # Modify `rgbWidget` self.__setVisibleComponents( rgbWidget, "rgbhsv" ) self.__setStaticComponent( rgbWidget, "g" ) self.__setColorFieldVisibility( rgbWidget, False ) + self.__setDynamicSliderBackgrounds( rgbWidget, False ) # Save defaults colorChooser = self.__colorChooserFromWidget( rgbWidget ) @@ -277,6 +300,8 @@ def testSaveDefaultOptions( self ) : self.assertEqual( self.__getStaticComponent( rgbaWidget ), "g" ) self.assertFalse( self.__getColorFieldVisibility( rgbWidget ) ) self.assertFalse( self.__getColorFieldVisibility( rgbaWidget ) ) + self.assertFalse( self.__getDynamicSliderBackgrounds( rgbWidget ) ) + self.assertFalse( self.__getDynamicSliderBackgrounds( rgbaWidget ) ) if __name__ == "__main__" : unittest.main() From 0e8887ce939e363380f6b43b5b2a8f1e9162a10f Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Wed, 23 Oct 2024 16:01:55 -0400 Subject: [PATCH 30/34] ColorChooser : Improve signal comments --- python/GafferUI/ColorChooser.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/python/GafferUI/ColorChooser.py b/python/GafferUI/ColorChooser.py index d69c7c0f0fa..eae5b34e882 100644 --- a/python/GafferUI/ColorChooser.py +++ b/python/GafferUI/ColorChooser.py @@ -962,23 +962,18 @@ def colorChangedSignal( self ) : ## A signal emitted whenever the visible components are changed. Slots # should have the signature slot( ColorChooser ). - # `visibleComponents` is a string representing the components currently - # visible. def visibleComponentsChangedSignal( self ) : return self.__visibleComponentsChangedSignal ## A signal emitted whenever the static component is changed. Slots # should have the signature slot( ColorChooser ). - # `staticComponent` is a single character string representing the - # current static component. def staticComponentChangedSignal( self ) : return self.__staticComponentChangedSignal ## A signal emitted whenever the visibility of the color field changes. # Slots should have the signature slot( ColorChooser ). - # `visible` is a boolean representing the current visibility. def colorFieldVisibleChangedSignal( self ) : return self.__colorFieldVisibleChangedSignal From a72e68e3384609f07ca4206480e30e86e504cb9c Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Wed, 23 Oct 2024 17:28:30 -0400 Subject: [PATCH 31/34] ColorChooser : Static colors on color field --- Changes.md | 2 +- python/GafferUI/ColorChooser.py | 26 ++++++++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/Changes.md b/Changes.md index a135f493d08..b836392d358 100644 --- a/Changes.md +++ b/Changes.md @@ -10,7 +10,7 @@ Improvements - DeletePoints : Added modes for deleting points based on a list of ids. - Light Editor, Attribute Editor, Spreadsheet : Add original and current color swatches to color popups. - SceneView : Added fallback framing extents to create a reasonable view when `SceneGadget` is empty, for example if the grid is hidden. -- ColorChooser : Added an option to toggle the dynamic update of colors displayed in the slider backgrounds. When enabled, the widget backgrounds update to show the color that will result from moving the indicator to a given position. When disabled, a static range of values is displayed instead. +- ColorChooser : Added an option to toggle the dynamic update of colors displayed in the slider and color field backgrounds. When enabled, the widget backgrounds update to show the color that will result from moving the indicator to a given position. When disabled, a static range of values is displayed instead. Fixes ----- diff --git a/python/GafferUI/ColorChooser.py b/python/GafferUI/ColorChooser.py index eae5b34e882..12d428befb8 100644 --- a/python/GafferUI/ColorChooser.py +++ b/python/GafferUI/ColorChooser.py @@ -232,7 +232,7 @@ def _displayTransformChanged( self ) : class _ColorField( GafferUI.Widget ) : - def __init__( self, color = imath.Color3f( 1.0 ), staticComponent = "v", **kw ) : + def __init__( self, color = imath.Color3f( 1.0 ), staticComponent = "v", dynamicBackground = True, **kw ) : GafferUI.Widget.__init__( self, QtWidgets.QWidget(), **kw ) @@ -251,6 +251,8 @@ def __init__( self, color = imath.Color3f( 1.0 ), staticComponent = "v", **kw ) self.__colorFieldToDraw = None self.setColor( color, staticComponent ) + self.setDynamicBackground( dynamicBackground ) + # Sets the color and the static component. `color` is in # RGB space for RGB static components, HSV space for # HSV static components and TMI space for TMI components. @@ -263,6 +265,16 @@ def getColor( self ) : return self.__color, self.__staticComponent + def setDynamicBackground( self, dynamicBackground ) : + + self.__dynamicBackground = dynamicBackground + self.__colorFieldToDraw = None + self._qtWidget().update() + + def getDynamicBackground( self ) : + + return self.__dynamicBackground + ## A signal emitted whenever a value has been changed. Slots should # have the signature slot( _ColorField, GafferUI.Slider.ValueChangedReason ) def valueChangedSignal( self ) : @@ -449,10 +461,15 @@ def __drawBackground( self, painter ) : xIndex, yIndex = self.__xyIndices() zIndex = self.__zIndex() - staticValue = self.__color[zIndex] - c = imath.Color3f() - c[zIndex] = staticValue + if self.__dynamicBackground or self.__staticComponent == "h" : + c[zIndex] = self.__color[zIndex] + elif self.__staticComponent in "rgbtm" : + c[zIndex] = 0.0 + elif self.__staticComponent in "sv" : + c[zIndex] = 1.0 + elif self.__staticComponent == "i" : + c[zIndex] = 0.5 ColorSpace = enum.Enum( "ColorSpace", [ "RGB", "HSV", "TMI" ] ) if self.__staticComponent in "rgb" : @@ -944,6 +961,7 @@ def setDynamicSliderBackgrounds( self, dynamic ) : for component, slider in self.__sliders.items() : slider.setDynamicBackground( dynamic ) + self.__colorField.setDynamicBackground( dynamic ) self.__dynamicSliderBackgroundsChangedSignal( self ) From a3d6614cf607128454c809c15408ce00763e12a7 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Wed, 23 Oct 2024 17:43:09 -0400 Subject: [PATCH 32/34] ColorChooser : Optimize color field redraws --- python/GafferUI/ColorChooser.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/python/GafferUI/ColorChooser.py b/python/GafferUI/ColorChooser.py index 12d428befb8..6d9c3eab4c6 100644 --- a/python/GafferUI/ColorChooser.py +++ b/python/GafferUI/ColorChooser.py @@ -302,7 +302,11 @@ def __setColorInternal( self, color, staticComponent, reason ) : return zIndex = self.__zIndex() - if color[zIndex] != self.__color[zIndex] or staticComponent != self.__staticComponent : + if ( + staticComponent != self.__staticComponent or + ( self.__dynamicBackground and color[zIndex] != self.__color[zIndex] ) or + ( not self.__dynamicBackground and staticComponent == "h" ) + ) : self.__colorFieldToDraw = None self.__color = color From ec79d5b9a37ba8afa1ee3543819edd41399bf2a3 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Thu, 24 Oct 2024 10:57:46 -0400 Subject: [PATCH 33/34] ColorChooser : Optimize slider redraws --- python/GafferUI/ColorChooser.py | 78 +++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/python/GafferUI/ColorChooser.py b/python/GafferUI/ColorChooser.py index 6d9c3eab4c6..d9f9f19d9bb 100644 --- a/python/GafferUI/ColorChooser.py +++ b/python/GafferUI/ColorChooser.py @@ -129,11 +129,18 @@ def __init__( self, color, component, dynamicBackground = True, **kw ) : self.color = color self.component = component self.__dynamicBackground = dynamicBackground + self.__gradientToDraw = None + self.__size = self.size() # Sets the slider color in RGB space for RGBA channels, # HSV space for HSV channels and TMI space for TMI channels. def setColor( self, color ) : + if ( + ( self.__dynamicBackground and color != self.color ) or + ( not self.__dynamicBackground and self.component == "s" ) + ) : + self.__gradientToDraw = None self.color = color self._qtWidget().update() @@ -144,6 +151,7 @@ def getColor( self ) : def setDynamicBackground( self, dynamicBackground ) : self.__dynamicBackground = dynamicBackground + self.__gradientToDraw = None self._qtWidget().update() def getDynamicBackground( self ) : @@ -153,43 +161,46 @@ def getDynamicBackground( self ) : def _drawBackground( self, painter ) : size = self.size() - grad = QtGui.QLinearGradient( 0, 0, size.x, 0 ) - displayTransform = self.displayTransform() + if self.__gradientToDraw is None or size != self.__size : + self.__gradientToDraw = QtGui.QLinearGradient( 0, 0, size.x, 0 ) - if self.component == "a" : - c1 = imath.Color3f( 0 ) - c2 = imath.Color3f( 1 ) - else : - if self.__dynamicBackground : - c1 = imath.Color3f( self.color[0], self.color[1], self.color[2] ) - elif self.component in "rgbvi" : + displayTransform = self.displayTransform() + + if self.component == "a" : c1 = imath.Color3f( 0 ) - elif self.component == "h" : - c1 = imath.Color3f( 0, 1, 1 ) - elif self.component == "s" : - c1 = imath.Color3f( self.color[0], 0, 1 ) - elif self.component in "tm" : - c1 = imath.Color3f( 0, 0, 0.5 ) - c2 = imath.Color3f( c1 ) - a = { "r" : 0, "g" : 1, "b" : 2, "h" : 0, "s" : 1, "v": 2, "t" : 0, "m" : 1, "i" : 2 }[self.component] - c1[a] = -1 if self.component in "tm" else 0 - c2[a] = 1 - - numStops = max( 2, size.x // 2 ) - for i in range( 0, numStops ) : - - t = float( i ) / (numStops-1) - c = c1 + (c2-c1) * t - if self.component in "hsv" : - c = c.hsv2rgb() - elif self.component in "tmi" : - c = _tmiToRGB( c ) - - grad.setColorAt( t, self._qtColor( displayTransform( c ) ) ) - - brush = QtGui.QBrush( grad ) + c2 = imath.Color3f( 1 ) + else : + if self.__dynamicBackground : + c1 = imath.Color3f( self.color[0], self.color[1], self.color[2] ) + elif self.component in "rgbvi" : + c1 = imath.Color3f( 0 ) + elif self.component == "h" : + c1 = imath.Color3f( 0, 1, 1 ) + elif self.component == "s" : + c1 = imath.Color3f( self.color[0], 0, 1 ) + elif self.component in "tm" : + c1 = imath.Color3f( 0, 0, 0.5 ) + c2 = imath.Color3f( c1 ) + a = { "r" : 0, "g" : 1, "b" : 2, "h" : 0, "s" : 1, "v": 2, "t" : 0, "m" : 1, "i" : 2 }[self.component] + c1[a] = -1 if self.component in "tm" else 0 + c2[a] = 1 + + numStops = max( 2, size.x // 2 ) + for i in range( 0, numStops ) : + + t = float( i ) / (numStops-1) + c = c1 + (c2-c1) * t + if self.component in "hsv" : + c = c.hsv2rgb() + elif self.component in "tmi" : + c = _tmiToRGB( c ) + + self.__gradientToDraw.setColorAt( t, self._qtColor( displayTransform( c ) ) ) + + brush = QtGui.QBrush( self.__gradientToDraw ) painter.fillRect( 0, 0, size.x, size.y, brush ) + self.__size = size def _drawValue( self, painter, value, position, state ) : @@ -228,6 +239,7 @@ def _drawValue( self, painter, value, position, state ) : def _displayTransformChanged( self ) : GafferUI.Slider._displayTransformChanged( self ) + self.__gradientToDraw = None self._qtWidget().update() class _ColorField( GafferUI.Widget ) : From d8458d80afb72d7d9af827ccd6075ab492efd6a5 Mon Sep 17 00:00:00 2001 From: Daniel Dresser Date: Thu, 7 Nov 2024 09:12:45 -0800 Subject: [PATCH 34/34] ImageWriter : Match Nuke view metadata when using Nuke layouts. This should address an issue where EXRs written from Gaffer using Nuke layouts sometimes did not load correctly in Nuke. Fixes #6120. --- Changes.md | 1 + python/GafferImageTest/ImageWriterTest.py | 3 - src/GafferImage/ImageWriter.cpp | 67 ++++++++++++++++++----- 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/Changes.md b/Changes.md index b836392d358..42f47729002 100644 --- a/Changes.md +++ b/Changes.md @@ -21,6 +21,7 @@ Fixes - LocalDispatcher : Fixed job status update when a job was killed _immediately_ after being launched. - `gaffer view` : Fixed default OpenColorIO display transform. - AnimationEditor : Fixed changing of the current frame by dragging the frame indicator or clicking on the time axis. +- ImageWriter : Matched view metadata to Nuke when using the Nuke options for `layout`. This should address an issue where EXRs written from Gaffer using Nuke layouts sometimes did not load correctly in Nuke (#6120). In the unlikely situation that you were relying on the old behaviour, you can set the env var `GAFFERIMAGE_IMAGEWRITER_OMIT_DEFAULT_NUKE_VIEW = 1` in order to keep the old behaviour. API --- diff --git a/python/GafferImageTest/ImageWriterTest.py b/python/GafferImageTest/ImageWriterTest.py index e7230a54205..269df2f35dc 100644 --- a/python/GafferImageTest/ImageWriterTest.py +++ b/python/GafferImageTest/ImageWriterTest.py @@ -1636,9 +1636,6 @@ def testWithChannelTestImage( self ): header = self.usefulHeader( writePath ) refHeader = self.usefulHeader( self.imagesPath() / ( "channelTest" + referenceFile + ".exr" ) ) - if layout == "Nuke/Interleave Channels": - # We don't match the view metadata which Nuke sticks on files without specific views - refHeader = list( filter( lambda i : i != 'view (type string): "main"', refHeader ) ) self.assertEqual( header, refHeader ) def testWithMultiViewChannelTestImage( self ): diff --git a/src/GafferImage/ImageWriter.cpp b/src/GafferImage/ImageWriter.cpp index 9f2753ddd01..9821164e4d7 100644 --- a/src/GafferImage/ImageWriter.cpp +++ b/src/GafferImage/ImageWriter.cpp @@ -1379,8 +1379,17 @@ std::string cleanExcessDots( std::string name ) return name; } +struct LayoutForChannel +{ + std::string partName; + std::string channelName; + std::string requestedDataType; + bool usesNukeView; +}; + + // Get the EXR names for the part, and channel for a Gaffer channel, along with the channel's data type -std::tuple< std::string, std::string, std::string > partChannelNameDataType( const ImageWriter *node, const StringPlug *dataTypePlug, const std::string &view, const std::string &gafferChannel, const std::vector< std::string > &viewNames, bool isDeep ) +LayoutForChannel evaluateLayoutForChannel( const ImageWriter *node, const StringPlug *dataTypePlug, const std::string &view, const std::string &gafferChannel, const std::vector< std::string > &viewNames, bool isDeep, bool testNukeView = false ) { std::string layer = ImageAlgo::layerName( gafferChannel ); std::string baseName = ImageAlgo::baseName( gafferChannel ); @@ -1456,11 +1465,36 @@ std::tuple< std::string, std::string, std::string > partChannelNameDataType( con namingContext.set( "imageWriter:nukeLayerName", &nukeLayerName ); namingContext.set( "imageWriter:nukeBaseName", &nukeBaseName ); - return std::make_tuple( + if( testNukeView ) + { + static const std::string nukeViewTestToken( "__nukeViewTest" ); + namingContext.set( "imageWriter:nukeViewName", &nukeViewTestToken ); + return { "", "", "", node->layoutPartNamePlug()->getValue().find( nukeViewTestToken ) != std::string::npos }; + } + + return { cleanExcessDots( node->layoutPartNamePlug()->getValue() ), cleanExcessDots( node->layoutChannelNamePlug()->getValue() ), - dataTypePlug ? dataTypePlug->getValue() : "" - ); + dataTypePlug ? dataTypePlug->getValue() : "", + false + }; +} + +bool testNukeView( const ImageWriter *node, const StringPlug *dataTypePlug, const std::string &gafferChannel ) +{ + // Do a special evaluation to test whether layoutPartName depends on the nukeViewName context + // variable, because if it does, we enable some Nuke specific behaviour ( Nuke sometimes names + // image parts after the view even if no views are used, and then those parts need to get their + // view metadata set ). + + // The alternative to this would be to disable substitutions on the plug, and do the substitutions + // ourselves with a custom Context::SubstitutionProvider that tracks whether nukeViewName is used. + // That wouldn't handle the case where layoutPartName depends on an expression that uses nukeViewName, + // though. + const std::vector defaultViews = { ImagePlug::defaultViewName }; + return evaluateLayoutForChannel( + node, dataTypePlug, ImagePlug::defaultViewName, gafferChannel, defaultViews, false, true + ).usesNukeView; } struct MetadataRegistration @@ -1952,9 +1986,8 @@ void ImageWriter::execute() const for( const string &i : channelsToWrite ) { - const auto & [ partName, channelName, requestedDataType ] = partChannelNameDataType( this, dataTypePlug, viewName, i, viewNames, defaultSpec.deep ); - - std::string dataType = requestedDataType; + LayoutForChannel layout = evaluateLayoutForChannel( this, dataTypePlug, viewName, i, viewNames, defaultSpec.deep ); + std::string dataType = layout.requestedDataType; if( depthDataTypeOverride != "" && ( i == "Z" || i == "ZBack" ) ) { dataType = depthDataTypeOverride; @@ -1968,17 +2001,14 @@ void ImageWriter::execute() const viewDataType = dataType; } - // Note that `partName = partName` is a hack to get around an issue with capturing from a - // structured binding. GCC allows it, but the C++17 spec doesn't, and it doesn't work in our - // Mac compiler. Once we're on C++20, it is explicitly supported, and we can remove the ` = partName` size_t partIndex = std::distance( parts.begin(), - std::find_if( parts.begin(), parts.end(), [partName = partName] (Part const& p) { return p.name == partName; } ) + std::find_if( parts.begin(), parts.end(), [&layout] (Part const& p) { return p.name == layout.partName; } ) ); if( partIndex >= parts.size() ) { - parts.push_back( { partName, { viewName }, defaultSpec, imageFormat, dataWindow, {}, {}, {} } ); + parts.push_back( { layout.partName, { viewName }, defaultSpec, imageFormat, dataWindow, {}, {}, {} } ); } else { @@ -1998,10 +2028,10 @@ void ImageWriter::execute() const } parts[ partIndex ].channels.push_back( viewName + "." + i ); - parts[ partIndex ].channelNames.push_back( channelName ); + parts[ partIndex ].channelNames.push_back( layout.channelName ); parts[ partIndex ].channelDataTypes.push_back( dataType ); - if( channelName == "A" ) + if( layout.channelName == "A" ) { hasAlpha = true; } @@ -2045,6 +2075,11 @@ void ImageWriter::execute() const } } + // \todo: deprecated, this variable can be treated as always true in the next major version, + // once we're confident no one is relying on the old behaviour. + static const char *nukeViewMetadataDeprecatedBehaviourString = getenv( "GAFFERIMAGE_IMAGEWRITER_OMIT_DEFAULT_NUKE_VIEW" ); + static const bool nukeViewMetadataDeprecatedBehaviour = nukeViewMetadataDeprecatedBehaviourString && std::string( nukeViewMetadataDeprecatedBehaviourString ) != "0"; + for( Part &part : parts ) { if( part.views.size() > 1 ) @@ -2069,6 +2104,10 @@ void ImageWriter::execute() const { part.spec.attribute("view", part.views[0] ); } + else if( ( !nukeViewMetadataDeprecatedBehaviour ) && testNukeView( this, dataTypePlug, part.channels[0] ) ) + { + part.spec.attribute("view", "main" ); + } if( part.name.size() ) {