Skip to content

Commit da53225

Browse files
Merge branch '1.6_maintenance'
2 parents 9202b1b + 64c4778 commit da53225

File tree

9 files changed

+471
-28
lines changed

9 files changed

+471
-28
lines changed

Changes.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,17 @@ Build
6363
1.6.x.x (relative to 1.6.12.0)
6464
=======
6565

66+
Improvements
67+
------------
68+
69+
- Crop : Added `Auto` mode for `areaSource`, automatically cropping to show only non-empty pixels.
70+
- GraphEditor : Improved responsiveness of select-drag, by deferring NodeEditor update until the drag ends.
71+
72+
API
73+
---
6674

75+
- Widget : Added `currentButtons()` static method. This returns the state of the mouse buttons during the last UI event to be processed.
76+
- LazyMethod : Added `deferUntilButtonRelease` option.
6777

6878
1.6.12.0 (relative to 1.6.11.1)
6979
========

include/GafferImage/Crop.h

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ class GAFFERIMAGE_API Crop : public ImageProcessor
6060
Area = 0,
6161
DataWindow = 1,
6262
DisplayWindow = 2,
63-
Format = 3
63+
Format = 3,
64+
Auto = 4
6465
};
6566

6667
Gaffer::IntPlug *areaSourcePlug();
@@ -95,6 +96,9 @@ class GAFFERIMAGE_API Crop : public ImageProcessor
9596
void hash( const Gaffer::ValuePlug *output, const Gaffer::Context *context, IECore::MurmurHash &h ) const override;
9697
void compute( Gaffer::ValuePlug *output, const Gaffer::Context *context ) const override;
9798

99+
Gaffer::ValuePlug::CachePolicy computeCachePolicy( const Gaffer::ValuePlug *output ) const override;
100+
Gaffer::ValuePlug::CachePolicy hashCachePolicy( const Gaffer::ValuePlug *output ) const override;
101+
98102
private :
99103

100104
Gaffer::AtomicBox2iPlug *cropWindowPlug();
@@ -106,6 +110,20 @@ class GAFFERIMAGE_API Crop : public ImageProcessor
106110
Gaffer::V2iPlug *offsetPlug();
107111
const Gaffer::V2iPlug *offsetPlug() const;
108112

113+
Gaffer::AtomicBox2iPlug *tileAutoAreaPlug();
114+
const Gaffer::AtomicBox2iPlug *tileAutoAreaPlug() const;
115+
116+
Gaffer::AtomicBox2iPlug *autoAreaPlug();
117+
const Gaffer::AtomicBox2iPlug *autoAreaPlug() const;
118+
119+
bool affectsTileAutoArea( const Gaffer::Plug *input ) const;
120+
void hashTileAutoArea( const Gaffer::Context *context, IECore::MurmurHash &h ) const;
121+
Imath::Box2i computeTileAutoArea( const Gaffer::Context *context ) const;
122+
123+
bool affectsAutoArea( const Gaffer::Plug *input ) const;
124+
void hashAutoArea( const Gaffer::Context *context, IECore::MurmurHash &h ) const;
125+
Imath::Box2i computeAutoArea( const Gaffer::Context *context ) const;
126+
109127
static size_t g_firstPlugIndex;
110128
};
111129

python/GafferImageTest/CropTest.py

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
##########################################################################
3737

3838
import unittest
39-
import os
39+
import random
4040
import imath
4141

4242
import IECore
@@ -305,5 +305,121 @@ def testFormatAffectsOutput( self ) :
305305
crop["format"].setValue( GafferImage.Format( 100, 200 ) )
306306
self.assertIn( crop["out"]["dataWindow"], { x[0] for x in cs } )
307307

308+
def testAutoArea( self ) :
309+
310+
# Rectangle will have a data window fitted perfectly to
311+
# the rectangle it draws.
312+
rectangle = GafferImage.Rectangle()
313+
314+
# Merging over a constant (even with zero alpha) will
315+
# destroy that data window.
316+
constant = GafferImage.Constant()
317+
constant["color"]["a"].setValue( 0 )
318+
319+
merge = GafferImage.Merge()
320+
merge["in"][0].setInput( constant["out"] )
321+
merge["in"][1].setInput( rectangle["out"] )
322+
323+
# But we can restore it using a crop with `AreaSource.Auto`.
324+
crop = GafferImage.Crop()
325+
crop["in"].setInput( merge["out"] )
326+
crop["areaSource"].setValue( crop.AreaSource.Auto )
327+
crop["affectDisplayWindow"].setValue( False )
328+
rectangle["lineWidth"].setValue( 0.5 )
329+
330+
random.seed( 100 )
331+
for i in range( 0, 10 ) :
332+
333+
area = imath.Box2f()
334+
for p in range( 0, 2 ) :
335+
area.extendBy( imath.V2f( random.uniform( 0, 1000 ), random.uniform( 0, 1000 ) ) )
336+
337+
with self.subTest( area = area ) :
338+
339+
rectangle["area"].setValue( area )
340+
self.assertEqual( crop["out"].dataWindow(), rectangle["out"].dataWindow() )
341+
342+
rectangle["color"].setValue( imath.Color4f( 0 ) )
343+
self.assertTrue( crop["out"].dataWindow().isEmpty() )
344+
345+
def testEmptyAutoArea( self ) :
346+
347+
constant = GafferImage.Constant()
348+
constant["color"]["a"].setValue( 0 )
349+
350+
crop = GafferImage.Crop()
351+
crop["in"].setInput( constant["out"] )
352+
crop["areaSource"].setValue( crop.AreaSource.Auto )
353+
crop["affectDisplayWindow"].setValue( False )
354+
355+
self.assertTrue( crop["out"].dataWindow().isEmpty() )
356+
357+
def testAutoAreaReusesTileResults( self ) :
358+
359+
constant = GafferImage.Constant()
360+
constant["color"].setValue( imath.Color4f( 1 ) )
361+
constant["format"].setValue(
362+
GafferImage.Format( int( GafferImage.ImagePlug.tileSize() * 2.5 ), int( GafferImage.ImagePlug.tileSize() * 3.5 ) )
363+
)
364+
365+
crop = GafferImage.Crop()
366+
crop["in"].setInput( constant["out"] )
367+
crop["areaSource"].setValue( crop.AreaSource.Auto )
368+
369+
# Limit to one thread to avoid concurrent computes of the same thing.
370+
with IECore.tbb_global_control(
371+
IECore.tbb_global_control.parameter.max_allowed_parallelism, 1
372+
) :
373+
# Compute the entire image.
374+
with Gaffer.PerformanceMonitor() as monitor :
375+
self.assertImagesEqual( crop["out"], crop["in"] )
376+
377+
# All tiles of the constant are the same, so there is only one compute.
378+
self.assertEqual( monitor.plugStatistics( constant["out"]["channelData"] ).computeCount, 1 )
379+
# The tile auto-areas can be shared for all tiles with the same local
380+
# crop window. Which gives us 4 unique results even though there are
381+
# 12 tiles.
382+
self.assertEqual( monitor.plugStatistics( crop["__tileAutoArea"] ).computeCount, 4 )
383+
384+
def testDeepAutoArea( self ) :
385+
386+
reader = GafferImage.ImageReader()
387+
reader["fileName"].setValue( self.imagesPath() / "representativeDeepImage.exr" )
388+
389+
slice = GafferImage.DeepSlice()
390+
slice["in"].setInput( reader["out"] )
391+
slice["nearClip"]["enabled"].setValue( True )
392+
slice["nearClip"]["value"].setValue( 2.86 )
393+
slice["farClip"]["enabled"].setValue( True )
394+
slice["farClip"]["value"].setValue( 3.6 )
395+
396+
crop = GafferImage.Crop()
397+
crop["in"].setInput( slice["out"] )
398+
crop["areaSource"].setValue( crop.AreaSource.Auto )
399+
crop["affectDisplayWindow"].setValue( False )
400+
401+
self.assertEqual(
402+
crop["out"].dataWindow(),
403+
imath.Box2i(
404+
imath.V2i( 14, 3 ),
405+
imath.V2i( 93, 56 )
406+
)
407+
)
408+
409+
def testChannelDataOnlyAffectsDataWindowForAutoArea( self ) :
410+
411+
constant = GafferImage.Constant()
412+
crop = GafferImage.Crop()
413+
crop["in"].setInput( constant["out"] )
414+
415+
cs = GafferTest.CapturingSlot( crop.plugDirtiedSignal() )
416+
constant["color"]["r"].setValue( 0.1 )
417+
self.assertNotIn( crop["out"]["dataWindow"], { x[0] for x in cs } )
418+
419+
crop["areaSource"].setValue( crop.AreaSource.Auto )
420+
del cs[:]
421+
constant["color"]["r"].setValue( 0.2 )
422+
self.assertIn( crop["out"]["dataWindow"], { x[0] for x in cs } )
423+
308424
if __name__ == "__main__":
309425
unittest.main()

python/GafferImageUI/CropUI.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,23 @@ def postCreate( node, menu ) :
7474

7575
"description" :
7676
"""
77-
Where to source the actual area to use. If this is
78-
set to DataWindow, it will use the input's Data Window,
79-
if it is set to DisplayWindow, it will use the input's
80-
Display Window, and if it is set to Area, it will use
81-
the Area plug.
77+
The source of the area to crop to.
78+
79+
- Area : A user-defined area specified by the `area` plug.
80+
- Format : A user-defined area specified by the `format` plug.
81+
- DataWindow : The data window of the input image.
82+
- DisplayWindow : The display window of the input image.
83+
- Auto : The minimal area that contains all the non-empty pixels
84+
of the input image. For flat images, this means pixels
85+
with a non-zero value in at least one channel, and for deep images
86+
it means pixels with at least one sample.
8287
""",
8388

8489
"preset:Area" : GafferImage.Crop.AreaSource.Area,
8590
"preset:Format" : GafferImage.Crop.AreaSource.Format,
8691
"preset:DataWindow" : GafferImage.Crop.AreaSource.DataWindow,
8792
"preset:DisplayWindow" : GafferImage.Crop.AreaSource.DisplayWindow,
93+
"preset:Auto" : GafferImage.Crop.AreaSource.Auto,
8894

8995
"plugValueWidget:type" : "GafferUI.PresetsPlugValueWidget",
9096

python/GafferUI/LazyMethod.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,12 @@ class LazyMethod( object ) :
4949
# Using `deferUntilPlaybackStops` requires that the widget has a
5050
# `scriptNode()` method that returns the appropriate ScriptNode from which
5151
# to acquire a Playback object.
52-
def __init__( self, deferUntilVisible = True, deferUntilIdle = True, deferUntilPlaybackStops = False, replacePendingCalls = True ) :
52+
def __init__( self, deferUntilVisible = True, deferUntilIdle = True, deferUntilPlaybackStops = False, replacePendingCalls = True, deferUntilButtonRelease = False ) :
5353

5454
self.__deferUntilVisible = deferUntilVisible
5555
self.__deferUntilIdle = deferUntilIdle
5656
self.__deferUntilPlaybackStops = deferUntilPlaybackStops
57+
self.__deferUntilButtonRelease = deferUntilButtonRelease
5758
self.__replacePendingCalls = replacePendingCalls
5859

5960
# Called to return the decorated method.
@@ -108,6 +109,10 @@ def wrapper( widget, *args, **kw ) :
108109
)
109110
)
110111

112+
elif self.__deferUntilButtonRelease and GafferUI.Widget.currentButtons() :
113+
114+
GafferUI.EventLoop.addIdleCallback( functools.partial( self.__idleForButtonRelease, weakref.ref( widget ), method ) )
115+
111116
elif self.__deferUntilIdle :
112117

113118
GafferUI.EventLoop.addIdleCallback( functools.partial( self.__idle, weakref.ref( widget ), method ) )
@@ -162,6 +167,27 @@ def __idle( cls, widgetWeakref, method ) :
162167

163168
return False # Remove idle callback
164169

170+
@classmethod
171+
def __idleForButtonRelease( cls, widgetWeakref, method ) :
172+
173+
# There's no global Qt signal for button releases that we can
174+
# hook into, so for now we do a busy-wait until all buttons are
175+
# released.
176+
## \todo If a lot of clients use `deferUntilButtonRelease` then
177+
# we should consider mechanisms for making this more efficient.
178+
179+
if GafferUI.Widget.currentButtons() :
180+
# Get called again in a bit.
181+
return True
182+
183+
widget = widgetWeakref()
184+
if widget is None or not GafferUI._qtObjectIsValid( widget._qtWidget() ):
185+
return
186+
187+
cls.__doPendingCalls( widget, method )
188+
189+
return False # Remove idle callback
190+
165191
@classmethod
166192
def __doPendingCalls( cls, widget, method ) :
167193

python/GafferUI/NodeSetEditor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ def __membersChanged( self, set, member ) :
276276

277277
self.__lazyUpdate()
278278

279-
@GafferUI.LazyMethod()
279+
@GafferUI.LazyMethod( deferUntilButtonRelease = True )
280280
def __lazyUpdate( self ) :
281281

282282
self._updateFromSet()

python/GafferUI/Widget.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,11 @@ def mousePosition( relativeTo=None ) :
680680

681681
return p
682682

683+
@staticmethod
684+
def currentButtons() :
685+
686+
return _eventFilter.currentButtons()
687+
683688
@staticmethod
684689
def currentModifiers() :
685690

@@ -969,6 +974,7 @@ def __init__( self ) :
969974
QtCore.QObject.__init__( self )
970975

971976
# variables used in the implementation of drag and drop.
977+
self.__virtualButton = GafferUI.ButtonEvent.Buttons.None_
972978
self.__lastButtonPressWidget = None
973979
self.__lastButtonPressEvent = None
974980
self.__dragDropEvent = None
@@ -1002,6 +1008,10 @@ def __init__( self ) :
10021008
QtCore.QEvent.Drop,
10031009
) )
10041010

1011+
def currentButtons( self ) :
1012+
1013+
return self.__virtualButtons( QtWidgets.QApplication.mouseButtons() )
1014+
10051015
def eventFilter( self, qObject, qEvent ) :
10061016

10071017
qEventType = qEvent.type() # for speed it's best not to keep calling this below
@@ -1144,27 +1154,23 @@ def __keyRelease( self, qObject, qEvent ) :
11441154

11451155
return False
11461156

1147-
def __virtualButtons( self, qtButtons ):
1148-
result = Widget._buttons( qtButtons )
1149-
if self.__dragDropEvent is not None and self.__dragDropEvent.__startedByKeyPress :
1150-
result |= GafferUI.ButtonEvent.Buttons.Left
1151-
return GafferUI.ButtonEvent.Buttons( result )
1157+
def __virtualButtons( self, qtButtons ) :
1158+
1159+
return GafferUI.ButtonEvent.Buttons( self.__virtualButton | Widget._buttons( qtButtons ) )
11521160

11531161
def __mouseButtonPress( self, qObject, qEvent ) :
11541162

1155-
if (
1156-
self.__dragDropEvent is not None and self.__dragDropEvent.__startedByKeyPress
1157-
and ( Widget._buttons( qEvent.button() ) & GafferUI.ButtonEvent.Buttons.Left )
1158-
) :
1159-
# We are doing a virtual drag based on a keypress, but once the actual mouse button gets pressed,
1160-
# we replace it with an actual drag.
1161-
self.__dragDropEvent.__startedByKeyPress = False
1163+
pressedButton = Widget._buttons( qEvent.button() )
1164+
if self.__dragDropEvent is not None and pressedButton == self.__virtualButton :
1165+
# We were doing a drag based on a virtual button press, but now the actual mouse button has been pressed,
1166+
# so we can replace it.
1167+
self.__virtualButton = GafferUI.ButtonEvent.Buttons.None_
11621168
return True
11631169

11641170
widget = Widget._owner( qObject )
11651171
if widget._buttonPressSignal is not None :
11661172
event = GafferUI.ButtonEvent(
1167-
Widget._buttons( qEvent.button() ),
1173+
pressedButton,
11681174
self.__virtualButtons( qEvent.buttons() ),
11691175
self.__widgetSpaceLine( qEvent, widget ),
11701176
0.0,
@@ -1337,7 +1343,7 @@ def __doDrag( self, qObject, qEvent ) :
13371343

13381344
return False
13391345

1340-
def __startDrag( self, qObject, qEvent, forKeyPress = False ) :
1346+
def __startDrag( self, qObject, qEvent, threshold = 3 ) :
13411347

13421348
if self.__lastButtonPressWidget is None :
13431349
return False
@@ -1357,7 +1363,6 @@ def __startDrag( self, qObject, qEvent, forKeyPress = False ) :
13571363
# the widget died
13581364
return False
13591365

1360-
threshold = 3 if not forKeyPress else 0
13611366
if ( self.__lastButtonPressEvent.line.p0 - self.__widgetSpaceLine( qEvent, sourceWidget ).p0 ).length() < threshold :
13621367
return False
13631368

@@ -1372,7 +1377,6 @@ def __startDrag( self, qObject, qEvent, forKeyPress = False ) :
13721377
)
13731378
dragDropEvent.sourceWidget = sourceWidget
13741379
dragDropEvent.destinationWidget = None
1375-
dragDropEvent.__startedByKeyPress = forKeyPress
13761380

13771381
dragData = sourceWidget._dragBeginSignal( sourceWidget, dragDropEvent )
13781382
if dragData is not None :
@@ -1519,9 +1523,12 @@ def __dragKeyPress( self, qObject, qKeyEvent ) :
15191523
qKeyEvent.modifiers()
15201524
)
15211525

1526+
self.__virtualButton = GafferUI.ButtonEvent.Buttons.Left
15221527
if self.__mouseButtonPress( qWidget, qEvent ) :
1523-
self.__startDrag( qWidget, qEvent, forKeyPress = True )
1528+
self.__startDrag( qWidget, qEvent, threshold = 0 )
15241529
return True
1530+
else :
1531+
self.__virtualButton = GafferUI.ButtonEvent.Buttons.None_
15251532

15261533
else :
15271534

@@ -1539,6 +1546,7 @@ def __dragKeyPress( self, qObject, qKeyEvent ) :
15391546
qKeyEvent.modifiers()
15401547
)
15411548

1549+
self.__virtualButton = GafferUI.ButtonEvent.Buttons.None_
15421550
self.__endDrag( self.__dragDropEvent.sourceWidget._qtWidget(), qEvent )
15431551
return True
15441552

0 commit comments

Comments
 (0)