Skip to content

Commit

Permalink
Merge pull request #6256 from ericmehl/annotationImprovements
Browse files Browse the repository at this point in the history
Annotation Improvements
  • Loading branch information
ericmehl authored Feb 10, 2025
2 parents e8f65bb + 15bc013 commit 3d2961b
Show file tree
Hide file tree
Showing 14 changed files with 386 additions and 77 deletions.
7 changes: 7 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ Features
- May be disabled entirely with `GafferScene.SceneAlgo.deregisterRenderAdaptor( "USDPointInstancerAdaptor" )`.
- Viewer : Added "Expand USD Instancers" item to the Expansion menu. Defaults to on for all renderers except OpenGL.
- PromotePointInstances : Added a new node for selectively converting a subset of a USD PointInstancer to expanded "hero" geometry.
- Annotations :
- Added copy and paste of annotations. The right-click menu of an annotation allows you to copy the annotation. Pressing <kbd>Control</kbd> + <kbd>V</kbd> in the Node Editor will paste the annotation to the selected nodes.
- Double clicking on an annotation now pops up the annotation editor dialogue.

Improvements
------------
Expand Down Expand Up @@ -43,6 +46,10 @@ API

- EditScopeAlgo : Added `renameRenderPass()` and `renameRenderPassNonEditableReason()` functions.
- SceneAlgo : Added `parallelGatherLocations()` function.
- GraphGadget : Added `annotationsGadget()` function.
- MetadataAlgo : Added `annotations()` variant accepting `Gaffer::Metadata::RegistrationTypes`. The default is `All` to match existing behavior and the previous `annotations()` variant is deprecated.
- AnnotationsGadget : Added `annotationAt()` function.
- AnnotationUI : Added `contextMenuSignal()` allowing customisations to the context menu for annotations.

1.5.4.1 (relative to 1.5.4.0)
=======
Expand Down
3 changes: 3 additions & 0 deletions include/Gaffer/MetadataAlgo.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
#pragma once

#include "Gaffer/Export.h"
#include "Gaffer/Metadata.h"
#include "Gaffer/Node.h"

#include "IECore/SimpleTypedData.h"
Expand Down Expand Up @@ -180,7 +181,9 @@ struct GAFFER_API Annotation
GAFFER_API void addAnnotation( Node *node, const std::string &name, const Annotation &annotation, bool persistent = true );
GAFFER_API Annotation getAnnotation( const Node *node, const std::string &name, bool inheritTemplate = false );
GAFFER_API void removeAnnotation( Node *node, const std::string &name );
[[deprecated( "Use alternative form with `RegistrationTypes` instead")]]
GAFFER_API void annotations( const Node *node, std::vector<std::string> &names );
GAFFER_API std::vector<std::string> annotations( const Node *node, Metadata::RegistrationTypes types = Metadata::RegistrationTypes::All );

/// Pass `user = false` for annotations not intended for creation directly by the user.
GAFFER_API void addAnnotationTemplate( const std::string &name, const Annotation &annotation, bool user = true );
Expand Down
16 changes: 16 additions & 0 deletions include/GafferUI/AnnotationsGadget.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@

#include "GafferUI/Gadget.h"

#include "IECoreGL/Selector.h"

#include "IECore/StringAlgo.h"

#include <unordered_map>
Expand Down Expand Up @@ -82,6 +84,11 @@ class GAFFERUI_API AnnotationsGadget : public Gadget

bool acceptsParent( const GraphComponent *potentialParent ) const override;

// Identifies an annotation by the node and it's name.
using AnnotationIdentifier = std::pair<const Gaffer::Node *, std::string>;
// Returns the node and annotation name under the specified line.
std::optional<AnnotationIdentifier> annotationAt( const IECore::LineSegment3f &lineInGadgetSpace ) const;

protected :

// Protected constructor and friend status so only GraphGadget can
Expand Down Expand Up @@ -148,6 +155,15 @@ class GAFFERUI_API AnnotationsGadget : public Gadget
// When we are hidden, we want to cancel all background tasks.
void visibilityChanged();

// Map associating an `IECoreGL::Selector::IDRender` entry with a `AnnotationIndex`.
using AnnotationBufferMap = std::unordered_map<unsigned int, AnnotationIdentifier>;

// If given an `AnnotationBufferMap` and `Selector`, draws all annotations
// with a unique `IDRender` index per annotation and fills `selectionIds`.
// If they are not given, no modification to the selection buffer IDs are
// made (all annotations have the ID for this widget).
void renderAnnotations( const Style *style, AnnotationBufferMap *selectionIds = nullptr ) const;

struct StandardAnnotation : public Gaffer::MetadataAlgo::Annotation
{
StandardAnnotation( const Gaffer::MetadataAlgo::Annotation &a, IECore::InternedString name ) : Annotation( a ), name( name ) {}
Expand Down
5 changes: 5 additions & 0 deletions include/GafferUI/GraphGadget.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

#pragma once

#include "GafferUI/AnnotationsGadget.h"
#include "GafferUI/ContainerGadget.h"
#include "GafferUI/ContextTracker.h"

Expand Down Expand Up @@ -133,6 +134,10 @@ class GAFFERUI_API GraphGadget : public ContainerGadget
AuxiliaryConnectionsGadget *auxiliaryConnectionsGadget();
const AuxiliaryConnectionsGadget *auxiliaryConnectionsGadget() const;

/// Returns the Gadget responsible for drawing annotations.
AnnotationsGadget *annotationsGadget();
const AnnotationsGadget *annotationsGadget() const;

/// Finds all the upstream NodeGadgets connected to the specified node
/// and appends them to the specified vector. Returns the new size of the vector.
/// \note Here "upstream" nodes are defined as nodes at the end of input
Expand Down
12 changes: 12 additions & 0 deletions python/GafferTest/MetadataAlgoTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,18 @@ def testAnnotations( self ) :
Gaffer.MetadataAlgo.Annotation( "abc", imath.Color3f( 0, 1, 0 ) )
)

def testAnnotationRegistrationTypes( self ) :

n = Gaffer.Node()
Gaffer.MetadataAlgo.addAnnotation( n, "persistent", Gaffer.MetadataAlgo.Annotation( "Always here", imath.Color3f( 1, 0, 0 ) ) )
Gaffer.MetadataAlgo.addAnnotation( n, "nonPersistent", Gaffer.MetadataAlgo.Annotation( "Ephemeral", imath.Color3f( 0, 1, 0 ) ), False )

Types = Gaffer.Metadata.RegistrationTypes
self.assertEqual( Gaffer.MetadataAlgo.annotations( n, Types.InstancePersistent ), ["persistent"] )
self.assertEqual( Gaffer.MetadataAlgo.annotations( n, Types.InstanceNonPersistent ), ["nonPersistent"] )
for t in [ Types.None_, Types.TypeId, Types.TypeIdDescendant ] :
self.assertEqual( Gaffer.MetadataAlgo.annotations( n, t ), [] )

def testAnnotationWithoutColor( self ) :

n = Gaffer.Node()
Expand Down
120 changes: 120 additions & 0 deletions python/GafferUI/AnnotationsUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import functools
import re
import imath
import weakref

import IECore

Expand Down Expand Up @@ -72,6 +73,125 @@ def __annotate( node, name, menu ) :
dialogue = __AnnotationsDialogue( node, name )
dialogue.wait( parentWindow = menu.ancestor( GafferUI.Window ) )

# A signal emitted when a popup menu for an annotation is about to be shown.
# This provides an opportunity to customize the menu from external code.
# The signature for slots is ( menuDefinition, annotation, persistent ) where
# `annotation` is a tuple of `( node, name )` and `persistent` indicates whether
# or not the annotation will be serialised with the script. Slots should modify
# `menuDefinition` in place.

__contextMenuSignal = Gaffer.Signals.Signal3()

def contextMenuSignal() :
return __contextMenuSignal

def __annotationIsPersistent( annotation ) :

node, name = annotation

persistentAnnotations = Gaffer.MetadataAlgo.annotations( node, Gaffer.Metadata.RegistrationTypes.InstancePersistent )
return name in persistentAnnotations

def __buttonPress( editorWeakRef, annotationsGadget, event ) :

if event.buttons & event.Buttons.Right :
annotation = annotationsGadget.annotationAt( event.line )
if annotation is None :
return False

menuDefinition = IECore.MenuDefinition()
contextMenuSignal()( menuDefinition, annotation, __annotationIsPersistent( annotation ) )

global __popupMenu
__popupMenu = GafferUI.Menu( menuDefinition )
__popupMenu.popup( editorWeakRef() )

return True

return True # Needed for `__buttonDoubleClick()` to fire

def __buttonDoubleClick( editorWeakRef, annotationsGadget, event ) :

if event.buttons == event.Buttons.Left :
annotation = annotationsGadget.annotationAt( event.line )
if annotation is None or not __annotationIsPersistent( annotation ) :
return False

node, name = annotation

__annotate( node, name, editorWeakRef() )

return True

return False

def __clipboardIsAnnotation( clipboard ) :

return (
isinstance( clipboard, IECore.CompoundData ) and
[ "color", "name", "text" ] == sorted( clipboard.keys() ) and
isinstance( clipboard["color"], IECore.Color3fData ) and
isinstance( clipboard["name"], IECore.StringData ) and
isinstance( clipboard["text"], IECore.StringData )
)

def __keyPress( editor, event ) :

if event.key == "V" and event.modifiers == event.modifiers.Control :
scriptNode = editor.scriptNode()
clipboard = scriptNode.ancestor( Gaffer.ApplicationRoot ).getClipboardContents()

if __clipboardIsAnnotation( clipboard ) :
with Gaffer.UndoScope( scriptNode ) :
editorSelection = [ i for i in scriptNode.selection() if editor.graphGadget().nodeGadget( i ) is not None ]
for n in editorSelection :
Gaffer.MetadataAlgo.addAnnotation(
n,
clipboard["name"].value,
Gaffer.MetadataAlgo.Annotation( clipboard["text"].value, clipboard["color"].value )
)
return True

return False

def __copyAnnotation( node, name ) :

annotation = Gaffer.MetadataAlgo.getAnnotation( node, name, True )

data = IECore.CompoundData(
{
"color" : IECore.Color3fData( annotation.color() ),
"name" : IECore.StringData( name ),
"text" : IECore.StringData( annotation.text() ),
}
)

node.scriptNode().ancestor( Gaffer.ApplicationRoot ).setClipboardContents( data )

def __contextMenu( menuDefinition, annotation, persistent ) :

node, name = annotation
menuDefinition.append(
"/Copy",
{
"command" : functools.partial( __copyAnnotation, node, name ),
"active" : persistent
},
)

def __graphEditorCreated( editor ) :
editor.graphGadget().annotationsGadget().buttonPressSignal().connect(
functools.partial( __buttonPress, weakref.ref( editor ) )
)
editor.graphGadget().annotationsGadget().buttonDoubleClickSignal().connect(
functools.partial( __buttonDoubleClick, weakref.ref( editor ) )
)
editor.keyPressSignal().connect( __keyPress )

GafferUI.GraphEditor.instanceCreatedSignal().connect( __graphEditorCreated )

contextMenuSignal().connect( __contextMenu )

class _AnnotationsHighlighter( GafferUI.CodeWidget.Highlighter ) :

__substitutionRe = re.compile( r"(\{[^}]+\})" )
Expand Down
4 changes: 2 additions & 2 deletions python/GafferUI/GraphEditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ def __preRender( self, viewportGadget ) :
def __annotationsMenu( self ) :

graphGadget = self.graphGadget()
annotationsGadget = graphGadget["__annotations"]
annotationsGadget = graphGadget.annotationsGadget()

annotations = Gaffer.MetadataAlgo.annotationTemplates() + [ "user", annotationsGadget.untemplatedAnnotations ]
visiblePattern = annotationsGadget.getVisibleAnnotations()
Expand Down Expand Up @@ -734,7 +734,7 @@ def appendMenuItem( annotation, label = None ) :

def __setVisibleAnnotations( self, unused, annotations ) :

annotationsGadget = self.graphGadget()["__annotations"]
annotationsGadget = self.graphGadget().annotationsGadget()
pattern = " ".join( a.replace( " ", r"\ " ) for a in annotations )
annotationsGadget.setVisibleAnnotations( pattern )

Expand Down
Loading

0 comments on commit 3d2961b

Please sign in to comment.