Hello. With my work on a simple Cubic Bezier investigation application
in PyQt (attached, obviously under GPL), I ran into a curious
behaviour of the slider/spin (for the time along the curve) when
controlled by the keyboard.

Steps:
1. Let the focus be either on the slider (on the left) or the spin
(below it). (The default slider/spin value is 0.50.)
3. Press up-arrow key to increase the slider/spin value.

Observation:
The value will not increase past 0.56.

Steps:
4. Press down-arrow key to decrease the slider/spin value until 0.30.
5. Press down-arrow once more.

Observation:
The value jumps down from 0.30 to 0.28 even though the precision is set at 0.01.

Step:
6. Press up-arrow.

Observation:
The value will now not rise above 0.28.

Step:
7. Adjust the slider position using the mouse.

Observation:
The value can change to any value in its full range from 0.00 to 1.00.

Step:
8. Adjust the slider using the mouse to go beyond 0.60.
9. Press down-arrow to decrease the value until 0.59.
10. Press down-arrow once more.

Observation:
11. The value jumps down to 0.56.
12. It will no longer go above 0.56 using the keyboard (as before).

Query:
My sliderMoved, spinChanged slots are straightforward, and just
convert the integer slider value to the spin and update the bezier
widget accordingly. In which case, I do not understand what it is I am
doing wrong in my programming. However, I wrote a minimal test where
the behaviour is not seen. Any guidance is appreciated.

Thanks!

-- 
Shriramana Sharma
#! /usr/bin/env python3

from PyQt4 . QtCore import *
from PyQt4 . QtGui import *
from math import sqrt

# float->string formatting function

def str_three_decimals ( a ) :
	return str ( int ( a * 1000 + 0.5 ) / 1000 )

# mathematical function

def quadraticroots ( a, b, c ) :
	if a == 0 :
		if b != 0 : return [ - c / b ]
		else : return [] # no valid equation so no roots
	det = b * b - 4 * a * c
	if det < 0 : return [] # only real roots will be returned
	if det == 0 : return [ - b / ( 2 * a ) ] # one root
	return [ ( - b - sqrt ( det ) ) / ( 2 * a ),
	         ( - b + sqrt ( det ) ) / ( 2 * a ) ]

# bezier analysis functions

def pointForTime ( t, p1, c1, c2, p2 ) :
	if t <= 0 : return p1
	if t >= 1 : return p2
	return p1 + ( c1 - p1 ) * 3 * t + ( c2 - c1 * 2 + p1 ) * 3 * t * t + ( p2 - c2 * 3 + c1 * 3 - p1 ) * t * t * t

def dirForTime   ( t, p1, c1, c2, p2 ) :
	if t < 0 : t = 0
	if t > 1 : t = 1
	return      ( c1 - p1 ) * 3     + ( c2 - c1 * 2 + p1 ) * 6 * t     + ( p2 - c2 * 3 + c1 * 3 - p1 ) * 3 * t * t

def accelForTime ( t, p1, c1, c2, p2 ) :
	if t < 0 : t = 0
	if t > 1 : t = 1
	return                            ( c2 - c1 * 2 + p1 ) * 6         + ( p2 - c2 * 3 + c1 * 3 - p1 ) * 6 * t

def timesOfCusp ( p1, c1, c2, p2 ) :
	a = c1 - p1
	b = c2 - c1 - a
	c = p2 - c2 - a - b * 2
	# dir = ( c * t * t + b * 2 * t + a ) * 3
	# at cusp, dir vector becomes null i.e. both x and y components are zero
	# qA = c ; qB = 2 * b ; qC = a
	rootsX = quadraticroots ( c . x (), b . x () * 2, a . x () )
	rootsY = quadraticroots ( c . y (), b . y () * 2, a . y () )
	cusps = []
	for x in rootsX :
		if x in rootsY : cusps += [ x ]
	return cusps

def timesOfInflection ( p1, c1, c2, p2 ) :

	# algorithm from http://www.caffeineowl.com/graphics/2d/vectorial/cubic-inflexion.html

	a = c1 - p1
	b = c2 - c1 - a
	c = p2 - c2 - a - b * 2
	
	qA = ( b . x () * c . y () - b . y () * c . x () )
	qB = ( a . x () * c . y () - a . y () * c . x () )
	qC = ( a . x () * b . y () - a . y () * b . x () )

	roots = quadraticroots ( qA, qB, qC )
	cusps = timesOfCusp ( p1, c1, c2, p2 )

	validroots = []
	for x in roots :
		if x > 0 and x < 1 and x not in cusps : validroots . append ( x )
	return validroots

# widgets

class BezierWidget ( QWidget ) :
	
	def __init__ ( self, parent = None ) :
		
		super ( BezierWidget, self ) . __init__ ( parent )

		self . setFixedSize ( 400, 400 )
		self . setMouseTracking ( True )
		
		self . p1 = QPointF ( 100, 150 )
		self . c1 = QPointF ( 166, 250 )
		self . c2 = QPointF ( 234, 250 )
		self . p2 = QPointF ( 300, 150 )

		self . bezTime = 0.5
		self . timePoint = pointForTime ( self . bezTime, self . p1, self . c1, self . c2, self . p2 )
		self . timeDir   = dirForTime   ( self . bezTime, self . p1, self . c1, self . c2, self . p2 )
		self . timeAccel = accelForTime ( self . bezTime, self . p1, self . c1, self . c2, self . p2 )
		self . calcInflectionAndCusp ()

		self . tweaking = False

		self . diamond = QPainterPath ()
		self . diamond . moveTo (  4,  0 )
		self . diamond . lineTo (  0,  4 )
		self . diamond . lineTo ( -4,  0 )
		self . diamond . lineTo (  0, -4 )
		self . diamond . closeSubpath ()
	
	def paintEvent ( self, event ) :
		
		painter = QPainter ( self )
		painter . setRenderHint ( QPainter . Antialiasing )
		painter . translate ( 0, 400 )
		painter . scale ( 1, -1 )
		
		palette = QApplication . palette ()
		bezierPen = QPen ( palette . text (), 1 )
		handlePen = QPen ( palette . highlight (), 2, Qt . DashLine )
		chandlePen = QPen ( palette . highlightedText (), 0.5, Qt . DashLine )

		handle1 = QPainterPath ()
		handle1 . moveTo ( self . p1 ) ; handle1 . lineTo ( self . c1 )
		painter . strokePath ( handle1, handlePen )

		handle2 = QPainterPath ()
		handle2 . moveTo ( self . p2 ) ; handle2 . lineTo ( self . c2 )
		painter . strokePath ( handle2, handlePen )

		chandle1 = QPainterPath ()
		chandle1 . moveTo ( self . p1 ) ; chandle1 . lineTo ( self . c2 )
		painter . strokePath ( chandle1, chandlePen )

		chandle2 = QPainterPath ()
		chandle2 . moveTo ( self . p2 ) ; chandle2 . lineTo ( self . c1 )
		painter . strokePath ( chandle2, chandlePen )

		bezier = QPainterPath ()
		bezier . moveTo ( self . p1 )
		bezier . cubicTo ( self . c1, self . c2, self . p2 )
		painter . strokePath ( bezier, bezierPen )

		for pt in ( self . p1, self . c1, self . c2, self . p2 ) :
			painter . save ()
			painter . translate ( pt )
			painter . fillPath ( self . diamond, palette . highlightedText () )
			painter . restore ()

		for i in range ( 9 ) :
			pt = pointForTime ( ( i + 1 ) / 10, self . p1, self . c1, self . c2, self . p2 )
			painter . save ()
			painter . translate ( pt )
			painter . scale ( 0.5, 0.5 )
			painter . fillPath ( self . diamond, palette . highlightedText () )
			painter . restore ()

		self . timePoint = pointForTime ( self . bezTime, self . p1, self . c1, self . c2, self . p2 )
		self . timeDir   = dirForTime   ( self . bezTime, self . p1, self . c1, self . c2, self . p2 )
		self . timeAccel = accelForTime ( self . bezTime, self . p1, self . c1, self . c2, self . p2 )
		
		dirLine   = QPainterPath ()
		dirLine   . lineTo ( self . timeDir   / 10 )
		accelLine = QPainterPath ()
		accelLine . lineTo ( self . timeAccel / 10 )

		painter . save ()
		painter . translate ( self . timePoint )
		painter . strokePath ( dirLine, QPen ( palette . highlight (), 2 ) )
		painter . strokePath ( accelLine, QPen ( palette . linkVisited (), 2 ) )
		painter . scale ( 1.5, 1.5 )
		painter . fillPath ( self . diamond, palette . highlightedText () )
		painter . restore ()

		for i in range ( len ( self . inflectionTimes ) ) :

			painter . save ()
			
			pt = pointForTime ( self . inflectionTimes [ i ], self . p1, self . c1, self . c2, self . p2 )
			painter . translate ( pt )
			painter . fillPath ( self . diamond, palette . linkVisited () )

			painter . restore ()

	def mousePressEvent ( self, event ) :
		pt = event . posF ()
		pt . setY ( 399 - pt . y () )
		offset = 5
		hotspot = QRectF ( - offset, - offset, offset * 2, offset * 2 )
		for x in ( self . p1, self . c1, self . c2, self . p2 ) :
			if hotspot . translated ( x ) . contains ( pt ) :
				self . tweaking = True
				if   x is self . p1 : self . tweakedPoint = "p1"
				elif x is self . c1 : self . tweakedPoint = "c1"
				elif x is self . c2 : self . tweakedPoint = "c2"
				elif x is self . p2 : self . tweakedPoint = "p2"
				return
	
	def mouseMoveEvent ( self, event ) :
		
		pt = event . posF ()
		if pt . x () < 0 : pt . setX ( 0 )
		if pt . x () > 399 : pt . setX ( 399 )
		if pt . y () < 0 : pt . setY ( 0 )
		if pt . y () > 399 : pt . setY ( 399 )
		
		ptInt = pt . toPoint ()
		QToolTip . showText ( self . mapToGlobal ( ptInt ), "%d,%d" % ( ptInt . x (), 399 - ptInt . y () ), self )

		if self . tweaking :
			
			pt . setY ( 399 - pt . y () )
			if   self . tweakedPoint == "p1" : self . p1 = pt
			elif self . tweakedPoint == "c1" : self . c1 = pt
			elif self . tweakedPoint == "c2" : self . c2 = pt
			elif self . tweakedPoint == "p2" : self . p2 = pt
			self . repaint ()
			self . emit ( SIGNAL ( "pointsModifiedInWidget ()" ) )
	
	def mouseReleaseEvent ( self, event ) :
		self . tweaking = False

	def calcInflectionAndCusp ( self ) :
		self . cuspTimes        = timesOfCusp       ( self . p1, self . c1, self . c2, self . p2 )
		self . inflectionTimes  = timesOfInflection ( self . p1, self . c1, self . c2, self . p2 )
		self . inflectionDirs   = [   dirForTime ( t, self . p1, self . c1, self . c2, self . p2 ) for t in self . inflectionTimes ]
		self . inflectionAccels = [ accelForTime ( t, self . p1, self . c1, self . c2, self . p2 ) for t in self . inflectionTimes ]
	
class MainWindow ( QWidget ) :

	def __init__ ( self, parent = None ) :

		super ( MainWindow, self ) . __init__ ( parent )

		self . setWindowTitle ( "Cubic Bezier Sandbox" )
		
		self . timeSlider = QSlider ()
		self . timeSlider . setMinimum ( 0 )
		self . timeSlider . setMaximum ( 100 )
		self . timeSlider . setValue ( 50 )

		self . timeSpin = QDoubleSpinBox ()
		self . timeSpin . setDecimals ( 2 )
		self . timeSpin . setMinimum ( 0 )
		self . timeSpin . setMaximum ( 1 )
		self . timeSpin . setSingleStep ( 0.01 )
		self . timeSpin . setValue ( 0.5 )

		self . lhsLayout = QVBoxLayout ()
		self . lhsLayout . addWidget ( self . timeSlider, 0, Qt . AlignHCenter )
		self . lhsLayout . addWidget ( self . timeSpin )

		self . bezierWidget = BezierWidget ()
		
		self . p1Label = QLabel ( "p1" )
		self . c1Label = QLabel ( "c1" )
		self . c2Label = QLabel ( "c2" )
		self . p2Label = QLabel ( "p2" )

		self . p1xSpin = QSpinBox ()
		self . p1ySpin = QSpinBox ()
		self . c1xSpin = QSpinBox ()
		self . c1ySpin = QSpinBox ()
		self . c2xSpin = QSpinBox ()
		self . c2ySpin = QSpinBox ()
		self . p2xSpin = QSpinBox ()
		self . p2ySpin = QSpinBox ()
		
		self . xyFieldsGrid = QGridLayout ()
		xyFieldsGridItems = ( ( self . p1Label, self . p1xSpin, self . p1ySpin ),
		                      ( self . c1Label, self . c1xSpin, self . c1ySpin ),
		                      ( self . c2Label, self . c2xSpin, self . c2ySpin ),
		                      ( self . p2Label, self . p2xSpin, self . p2ySpin ) )
		for i in range ( 4 ) :
			for j in range ( 3 ) :
				self . xyFieldsGrid . addWidget ( xyFieldsGridItems [ i ] [ j ], i, j )
				if j > 0 : # isinstance ( xyFieldsGridItems [ i ] [ j ], QSpinBox ) would be pedantically correct
					xyFieldsGridItems [ i ] [ j ] . setMaximum ( 399 )
		
		self . inflectTimeLabel   = QLabel ( "Inflection times" )
		self . inflectTime1Label  = QLabel ()
		self . inflectTime2Label  = QLabel ()
		self . inflDirLabel       = QLabel ( "Inflection dirs" )
		self . inflDirValLabels   = [ [ QLabel (), QLabel () ], [ QLabel (), QLabel () ] ]
		self . inflAccelLabel     = QLabel ( "Inflection accels" )
		self . inflAccelValLabels = [ [ QLabel (), QLabel () ], [ QLabel (), QLabel () ] ]
		self . timePointLabel     = QLabel ( "Point for time" )
		self . timePointXLabel    = QLabel ()
		self . timePointYLabel    = QLabel ()
		self . timeDirLabel       = QLabel ( "Velocity for time" )
		self . timeDirXLabel      = QLabel ()
		self . timeDirYLabel      = QLabel ()
		self . timeAccelLabel     = QLabel ( "Acceleration for time" )
		self . timeAccelXLabel    = QLabel ()
		self . timeAccelYLabel    = QLabel ()
		
		self . otherFieldsGrid = QGridLayout ()
		a = self . otherFieldsGrid . addWidget
		a ( self . inflectTimeLabel, 0, 0, 1, 2 )
		a ( self . inflectTime1Label, 1, 0 )
		a ( self . inflectTime2Label, 1, 1 )
		a ( self . inflDirLabel, 2, 0, 1, 2 )
		for i in range ( 2 ) :
			for j in range ( 2 ) :
				a ( self . inflDirValLabels [ i ] [ j ], 3 + i, j )
		a ( self . inflAccelLabel, 5, 0, 1, 2 )
		for i in range ( 2 ) :
			for j in range ( 2 ) :
				a ( self . inflAccelValLabels [ i ] [ j ], 6 + i, j )
		a ( self . timePointLabel, 8, 0, 1, 2 )
		a ( self . timePointXLabel, 9, 0 )
		a ( self . timePointYLabel, 9, 1 )
		a ( self . timeDirLabel, 10, 0, 1, 2 )
		a ( self . timeDirXLabel, 11, 0 )
		a ( self . timeDirYLabel, 11, 1 )
		a ( self . timeAccelLabel, 12, 0, 1, 2 )
		a ( self . timeAccelXLabel, 13, 0 )
		a ( self . timeAccelYLabel, 13, 1 )
		
		self . rhsLayout = QVBoxLayout ()
		self . rhsLayout . addLayout ( self . xyFieldsGrid )
		self . rhsLayout . addLayout ( self . otherFieldsGrid )
		self . rhsLayout . addStretch ()

		self . mainLayout = QHBoxLayout ( self )
		self . mainLayout . addLayout ( self . lhsLayout )
		self . mainLayout . addWidget ( self . bezierWidget )
		self . mainLayout . addLayout ( self . rhsLayout )

		QObject . connect ( self . timeSlider, SIGNAL ( "valueChanged ( int )" ), self . sliderMoved )
		QObject . connect ( self . timeSpin, SIGNAL ( "valueChanged ( double )" ), self . spinChanged )
		
		QObject . connect ( self . bezierWidget, SIGNAL ( "pointsModifiedInWidget ()" ), self . updateXYFields )
		QObject . connect ( self . bezierWidget, SIGNAL ( "pointsModifiedInWidget ()" ), self . updateAnalysisFields )
		self . updateXYFields ()
		self . updateAnalysisFields ()

		for x in self . children () :
			if isinstance ( x, QSpinBox ) :
				QObject . connect ( x, SIGNAL ( "valueChanged ( int )" ), self . updateBezierWidgetData )
				QObject . connect ( x, SIGNAL ( "valueChanged ( int )" ), self . bezierWidget . repaint )
				QObject . connect ( x, SIGNAL ( "valueChanged ( int )" ), self . updateAnalysisFields )
				# note that updateAnalysisFields takes the data from the bezier widget so it should also come after updateBezierWidgetData

	def sliderMoved ( self, timeScaled ) :
		self . timeSpin . setValue ( timeScaled / 100 )
		self . bezierWidget . bezTime = timeScaled / 100
		self . bezierWidget . repaint ()
		self . updateAnalysisFields ()

	def spinChanged ( self, time ) :
		self . timeSlider . setValue ( time * 100 )
		self . bezierWidget . bezTime = time
		self . bezierWidget . repaint ()
		self . updateAnalysisFields ()

	def updateXYFields ( self ) :
		bezierSpinMap = ( ( self . bezierWidget . p1, self . p1xSpin, self . p1ySpin ),
		                  ( self . bezierWidget . c1, self . c1xSpin, self . c1ySpin ),
		                  ( self . bezierWidget . c2, self . c2xSpin, self . c2ySpin ),
		                  ( self . bezierWidget . p2, self . p2xSpin, self . p2ySpin ) )
		for mapitem in bezierSpinMap :
			mapitem [ 1 ] . setValue ( mapitem [ 0 ] . x () )
			mapitem [ 2 ] . setValue ( mapitem [ 0 ] . y () )

	def updateBezierWidgetData ( self ) :
		bw = self . bezierWidget
		bezierSpinMap = ( ( bw . p1, self . p1xSpin, self . p1ySpin ),
		                  ( bw . c1, self . c1xSpin, self . c1ySpin ),
		                  ( bw . c2, self . c2xSpin, self . c2ySpin ),
		                  ( bw . p2, self . p2xSpin, self . p2ySpin ) )
		for mapitem in bezierSpinMap :
			mapitem [ 0 ] . setX ( mapitem [ 1 ] . value () )
			mapitem [ 0 ] . setY ( mapitem [ 2 ] . value () )
		bw . calcInflectionAndCusp ()

	def updateAnalysisFields ( self ) :
		bw = self . bezierWidget
		if len ( bw . inflectionTimes ) == 0 :
			self . inflectTime1Label . setText ( "-" )
			self . inflectTime2Label . setText ( "-" )
			for row in self . inflDirValLabels :
				for x in row : x . setText ( "-" )
			for row in self . inflAccelValLabels :
				for x in row : x . setText ( "-" )
		elif len ( bw . inflectionTimes ) == 1 :
			self . inflectTime1Label . setText ( str_three_decimals ( bw . inflectionTimes [ 0 ] ) )
			self . inflectTime2Label . setText ( "-" )
			d = self . inflDirValLabels
			d [ 0 ] [ 0 ] . setText ( str_three_decimals ( bw . inflectionDirs [ 0 ] . x () ) )
			d [ 0 ] [ 1 ] . setText ( str_three_decimals ( bw . inflectionDirs [ 0 ] . y () ) )
			d [ 1 ] [ 0 ] . setText ( "-" )
			d [ 1 ] [ 1 ] . setText ( "-" )
			a = self . inflAccelValLabels
			a [ 0 ] [ 0 ] . setText ( str_three_decimals ( bw . inflectionAccels [ 0 ] . x () ) )
			a [ 0 ] [ 1 ] . setText ( str_three_decimals ( bw . inflectionAccels [ 0 ] . y () ) )
			a [ 1 ] [ 0 ] . setText ( "-" )
			a [ 1 ] [ 1 ] . setText ( "-" )
		elif len ( bw . inflectionTimes ) == 2 :
			self . inflectTime1Label . setText ( str_three_decimals ( bw . inflectionTimes [ 0 ] ) )
			self . inflectTime2Label . setText ( str_three_decimals ( bw . inflectionTimes [ 1 ] ) )
			for i in ( 0, 1 ) :
				self . inflDirValLabels   [ i ] [ 0 ] . setText ( str_three_decimals ( bw . inflectionDirs   [ i ] . x () ) )
				self . inflDirValLabels   [ i ] [ 1 ] . setText ( str_three_decimals ( bw . inflectionDirs   [ i ] . y () ) )
				self . inflAccelValLabels [ i ] [ 0 ] . setText ( str_three_decimals ( bw . inflectionAccels [ i ] . x () ) )
				self . inflAccelValLabels [ i ] [ 1 ] . setText ( str_three_decimals ( bw . inflectionAccels [ i ] . y () ) )
		for ( v, x, y ) in ( ( bw . timePoint, self . timePointXLabel, self . timePointYLabel ),
		                     ( bw . timeDir  , self . timeDirXLabel  , self . timeDirYLabel   ),
		                     ( bw . timeAccel, self . timeAccelXLabel, self . timeAccelYLabel ) ) :
			x . setText ( str_three_decimals ( v . x () ) )
			y . setText ( str_three_decimals ( v . y () ) )

app = QApplication ( [] )
mainWindow = MainWindow ()
mainWindow . show ()
app . exec_ ()
#! /usr/bin/env python3

from PyQt4 . QtCore import *
from PyQt4 . QtGui import *

class MainWindow ( QWidget ) :
	
	def __init__ ( self, parent = None ) :

		super ( MainWindow, self ) . __init__ ( parent )

		self . slider = QSlider ()
		self . slider . setMinimum ( 0 )
		self . slider . setMaximum ( 100 )
		self . slider . setValue ( 50 )

		self . spin = QSpinBox ()
		self . spin . setMinimum ( 0 )
		self . spin . setMaximum ( 100 )
		self . spin . setValue ( 50 )
		
		self . dblSpin = QDoubleSpinBox ()
		self . dblSpin . setDecimals ( 2 )
		self . dblSpin . setMinimum ( 0 )
		self . dblSpin . setMaximum ( 1 )
		self . dblSpin . setSingleStep ( 0.01 )
		self . dblSpin . setValue ( 0.5 )

		self . layout = QVBoxLayout ()
		self . layout . addWidget ( self . slider, 0, Qt . AlignHCenter )
		self . layout . addWidget ( self . spin )
		self . layout . addWidget ( self . dblSpin )

		self . setLayout ( self . layout )

		QObject . connect ( self . slider, SIGNAL ( "valueChanged ( int )" ), self . sliderMoved )
		QObject . connect ( self . spin, SIGNAL ( "valueChanged ( int )" ), self . spinChanged )
		QObject . connect ( self . dblSpin, SIGNAL ( "valueChanged ( double )" ), self . dblSpinChanged )

	def sliderMoved ( self, scaledTime ) :
		self . spin . setValue ( scaledTime )
		self . dblSpin . setValue ( scaledTime / 100 )

	def spinChanged ( self, scaledTime ) :
		self . slider . setValue ( scaledTime )
		self . dblSpin . setValue ( scaledTime / 100 )

	def dblSpinChanged ( self, time ) :
		self . slider . setValue ( time * 100 )
		self . spin . setValue ( time * 100 )

app = QApplication ( [] )
window = MainWindow ()
window . show ()
app . exec_ ()
_______________________________________________
PyQt mailing list    PyQt@riverbankcomputing.com
http://www.riverbankcomputing.com/mailman/listinfo/pyqt

Reply via email to