Skip to content

Commit

Permalink
ColorChooser : Add TMI sliders
Browse files Browse the repository at this point in the history
  • Loading branch information
ericmehl committed Jul 16, 2024
1 parent 1936a34 commit 5ce407d
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 23 deletions.
2 changes: 1 addition & 1 deletion SConstruct
Original file line number Diff line number Diff line change
Expand Up @@ -1075,7 +1075,7 @@ libraries = {

"GafferUITest" : {

"additionalFiles" : glob.glob( "python/GafferUITest/scripts/*.gfr" ),
"additionalFiles" : glob.glob( "python/GafferUITest/scripts/*.gfr" ) + glob.glob( "python/GafferUITest/images/*" ),

},

Expand Down
112 changes: 90 additions & 22 deletions python/GafferUI/ColorChooser.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,24 +44,55 @@

from Qt import QtGui

__tmi2rgbMatrix = imath.M33f(
-1.0 / 2.0, 0.0, 1.0 / 2.0,
1.0 / 3.0, -2.0 / 3.0, 1.0 / 3.0,
1.0, 1.0, 1.0
)
__rgb2tmiMatrix = __tmi2rgbMatrix.inverse()

def _tmi2rgb( c ) :
rgb = imath.V3f( c.r, c.g, c.b ) * __tmi2rgbMatrix

result = c.__class__( c )
result.r = rgb.x
result.g = rgb.y
result.b = rgb.z

return result

def _rgb2tmi( c ) :
tmi = imath.V3f( c.r, c.g, c.b ) * __rgb2tmiMatrix

result = c.__class__( c )
result.r = tmi.x
result.g = tmi.y
result.b = tmi.z

return result

# A custom slider for drawing the backgrounds.
class _ComponentSlider( GafferUI.Slider ) :

def __init__( self, color, component, **kw ) :

min = hardMin = 0
max = hardMax = 1
if component in "tm" :
min = hardMin = -1
max = hardMax = 1
else :
min = hardMin = 0
max = hardMax = 1

if component in ( "r", "g", "b", "v" ) :
if component in ( "r", "g", "b", "v", "i" ) :
hardMax = sys.float_info.max

GafferUI.Slider.__init__( self, 0.0, min, max, hardMin, hardMax, **kw )

self.color = color
self.component = component

# Sets the slider color in RGB space for RGBA channels and
# HSV space for HSV channels.
# 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 ) :

self.color = color
Expand All @@ -84,8 +115,8 @@ def _drawBackground( self, painter ) :
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] )
a = { "r" : 0, "g" : 1, "b" : 2, "h" : 0, "s" : 1, "v": 2 }[self.component]
c1[a] = 0
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 )
Expand All @@ -95,6 +126,8 @@ def _drawBackground( self, painter ) :
c = c1 + (c2-c1) * t
if self.component in "hsv" :
c = c.hsv2rgb()
elif self.component in "tmi" :
c = _tmi2rgb( c )

grad.setColorAt( t, self._qtColor( displayTransform( c ) ) )

Expand All @@ -110,6 +143,8 @@ class ColorChooser( GafferUI.Widget ) :

ColorChangedReason = enum.Enum( "ColorChangedReason", [ "Invalid", "SetColor", "Reset" ] )

__ColorSpace = enum.Enum( "__ColorSpace", [ "RGB", "HSV", "TMI" ] )

def __init__( self, color=imath.Color3f( 1 ), **kw ) :

self.__column = GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Vertical, spacing = 4 )
Expand All @@ -118,6 +153,7 @@ def __init__( self, color=imath.Color3f( 1 ), **kw ) :

self.__color = color
self.__colorHSV = self.__color.rgb2hsv()
self.__colorTMI = _rgb2tmi( self.__color )
self.__defaultColor = color

self.__sliders = {}
Expand All @@ -127,7 +163,7 @@ def __init__( self, color=imath.Color3f( 1 ), **kw ) :
with self.__column :

# sliders and numeric widgets
for component in "rgbahsv" :
for component in "rgbahsvtmi" :
with GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) :

with GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 8 ) :
Expand Down Expand Up @@ -229,8 +265,11 @@ def __componentValueChanged( self, componentWidget, reason ) :
# doesn't provide the capability itself. Add the functionality
# into the NumericWidget and remove this code.
componentValue = componentWidget.getValue()
componentValue = max( componentValue, 0 )
if componentWidget.component in ( "a", "h", "s" ) :
if componentWidget.component in "tm" :
componentValue = max( componentValue, -1 )
else :
componentValue = max( componentValue, 0 )
if componentWidget.component in ( "a", "h", "s", "t", "m" ) :
componentValue = min( componentValue, 1 )

if componentWidget.component in ( "r", "g", "b", "a" ) :
Expand All @@ -240,15 +279,22 @@ def __componentValueChanged( self, componentWidget, reason ) :
newColor[a] = componentValue

self.__setColorInternal( newColor, reason )
else :
elif componentWidget.component in ( "h", "s", "v" ) :
newColor = self.__colorHSV.__class__( self.__colorHSV )

a = { "h" : 0, "s" : 1, "v" : 2 }[componentWidget.component]
newColor[a] = componentValue

self.__setColorInternal( newColor, reason, True )
self.__setColorInternal( newColor, reason, self.__ColorSpace.HSV )
elif componentWidget.component in ( "t", "m", "i" ) :
newColor = self.__colorTMI.__class__( self.__colorTMI )

def __setColorInternal( self, color, reason, hsv = False ) :
a = { "t" : 0, "m" : 1, "i" : 2 }[componentWidget.component]
newColor[a] = componentValue

self.__setColorInternal( newColor, reason, self.__ColorSpace.TMI )

def __setColorInternal( self, color, reason, colorSpace = __ColorSpace.RGB ) :

dragBeginOrEnd = reason in (
GafferUI.Slider.ValueChangedReason.DragBegin,
Expand All @@ -257,19 +303,34 @@ def __setColorInternal( self, color, reason, hsv = False ) :
GafferUI.NumericWidget.ValueChangedReason.DragEnd,
)

colorChanged = color != ( self.__colorHSV if hsv else self.__color )
colorChanged = color != {
self.__ColorSpace.RGB : self.__color,
self.__ColorSpace.HSV : self.__colorHSV,
self.__ColorSpace.TMI : self.__colorTMI
}[colorSpace]

if colorChanged :
colorRGB = color.hsv2rgb() if hsv else color
self.__color = colorRGB
self.__colorSwatch.setColor( colorRGB )
if colorSpace == self.__ColorSpace.RGB :
colorRGB = color
colorHSV = color.rgb2hsv()
colorTMI = _rgb2tmi( colorRGB )
elif colorSpace == self.__ColorSpace.HSV :
colorRGB = color.hsv2rgb()
colorHSV = color
colorTMI = _rgb2tmi( colorRGB )
elif colorSpace == self.__ColorSpace.TMI :
colorRGB = _tmi2rgb( color )
colorHSV = colorRGB.rgb2hsv()
colorTMI = color

colorHSV[0] = colorHSV[0] if colorHSV[1] > 1e-7 and colorHSV[2] > 1e-7 else self.__colorHSV[0]
colorHSV[1] = colorHSV[1] if colorHSV[2] > 1e-7 else self.__colorHSV[1]

hsv = color if hsv else color.rgb2hsv()

hsv[0] = hsv[0] if hsv[1] > 1e-7 and hsv[2] > 1e-7 else self.__colorHSV[0]
hsv[1] = hsv[1] if hsv[2] > 1e-7 else self.__colorHSV[1]
self.__color = colorRGB
self.__colorHSV = colorHSV
self.__colorTMI = colorTMI

self.__colorHSV = hsv
self.__colorSwatch.setColor( colorRGB )

## \todo This is outside the conditional because the clamping we do
# in __componentValueChanged means the color value may not correspond
Expand Down Expand Up @@ -309,3 +370,10 @@ def __updateUIFromColor( self ) :
for component, index in ( ( "h", 0 ), ( "s", 1 ), ( "v", 2 ) ) :
self.__sliders[component].setValue( self.__colorHSV[index] )
self.__numericWidgets[component].setValue( self.__colorHSV[index] )

for slider in [ v for k, v in self.__sliders.items() if k in "tmi" ] :
slider.setColor( self.__colorTMI )

for component, index in ( ( "t", 0 ), ( "m", 1 ), ( "i", 2 ) ) :
self.__sliders[component].setValue( self.__colorTMI[index] )
self.__numericWidgets[component].setValue( self.__colorTMI[index] )
83 changes: 83 additions & 0 deletions python/GafferUITest/ColorChooserTest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
##########################################################################
#
# 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 pathlib
import unittest

import imath

import IECoreImage

import Gaffer
import GafferUI
from GafferUI.ColorChooser import _tmi2rgb
from GafferUI.ColorChooser import _rgb2tmi
import GafferUITest

class ColorChooserTest( GafferUITest.TestCase ) :

def testTMI( self ) :
# Load a precomputed image generated by Houdini to test our TMI <-> RGB calculation.
# The image is 200px x 20px. Each 20px square varies the temperature from -1.0 to 1.0
# on the X axis and magenta from -1.0 to 1.0 on the Y-axis. Each of the 10 squares, from
# left to right, varies the intensity from 0.0 to 0.9.
tileWidth = 20
tileCount = 10
tmiTarget = IECoreImage.ImageReader( ( pathlib.Path( __file__ ).parent / "images" / "tmi.exr" ).as_posix() ).read()

for tile in range ( 0, tileCount ) :
for y in range( 0, tileWidth ) :
for x in range( 0, tileWidth ) :
tmiOriginal = imath.Color3f(
( float( x ) / tileWidth ) * 2.0 - 1.0,
( float( y ) / tileWidth ) * 2.0 - 1.0,
float( tile ) / tileCount
)

rgbConverted = _tmi2rgb( tmiOriginal )
index = y * ( tileWidth * tileCount ) + ( tile * tileWidth + x )
self.assertAlmostEqual( tmiTarget["R"][index], rgbConverted.r, places = 6 )
self.assertAlmostEqual( tmiTarget["G"][index], rgbConverted.g, places = 6 )
self.assertAlmostEqual( tmiTarget["B"][index], rgbConverted.b, places = 6 )

tmiConverted = _rgb2tmi( rgbConverted )
self.assertAlmostEqual( tmiConverted.r, tmiOriginal.r, places = 6 )
self.assertAlmostEqual( tmiConverted.g, tmiOriginal.g, places = 6 )
self.assertAlmostEqual( tmiConverted.b, tmiOriginal.b, places = 6 )


if __name__ == "__main__" :
unittest.main()
1 change: 1 addition & 0 deletions python/GafferUITest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
from .BoxIOUITest import BoxIOUITest
from .AnnotationsGadgetTest import AnnotationsGadgetTest
from .PopupWindowTest import PopupWindowTest
from .ColorChooserTest import ColorChooserTest

if __name__ == "__main__":
unittest.main()
Binary file added python/GafferUITest/images/tmi.exr
Binary file not shown.

0 comments on commit 5ce407d

Please sign in to comment.