diff --git a/SRTTransform3D.py b/SRTTransform3D.py
index 94c3df77..89b8ab13 100644
--- a/SRTTransform3D.py
+++ b/SRTTransform3D.py
@@ -6,12 +6,12 @@ import pyqtgraph as pg
import numpy as np
import scipy.linalg
-class SRTTransform3D(QtGui.QMatrix4x4):
+class SRTTransform3D(pg.Transform3D):
"""4x4 Transform matrix that can always be represented as a combination of 3 matrices: scale * rotate * translate
This transform has no shear; angles are always preserved.
"""
def __init__(self, init=None):
- QtGui.QMatrix4x4.__init__(self)
+ pg.Transform3D.__init__(self)
self.reset()
if init is None:
return
@@ -190,11 +190,11 @@ class SRTTransform3D(QtGui.QMatrix4x4):
self.update()
def update(self):
- QtGui.QMatrix4x4.setToIdentity(self)
+ pg.Transform3D.setToIdentity(self)
## modifications to the transform are multiplied on the right, so we need to reverse order here.
- QtGui.QMatrix4x4.translate(self, *self._state['pos'])
- QtGui.QMatrix4x4.rotate(self, self._state['angle'], *self._state['axis'])
- QtGui.QMatrix4x4.scale(self, *self._state['scale'])
+ pg.Transform3D.translate(self, *self._state['pos'])
+ pg.Transform3D.rotate(self, self._state['angle'], *self._state['axis'])
+ pg.Transform3D.scale(self, *self._state['scale'])
def __repr__(self):
return str(self.saveState())
diff --git a/Transform3D.py b/Transform3D.py
new file mode 100644
index 00000000..aa948e28
--- /dev/null
+++ b/Transform3D.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+from .Qt import QtCore, QtGui
+import pyqtgraph as pg
+import numpy as np
+
+class Transform3D(QtGui.QMatrix4x4):
+ """
+ Extension of QMatrix4x4 with some helpful methods added.
+ """
+ def __init__(self, *args):
+ QtGui.QMatrix4x4.__init__(self, *args)
+
+ def matrix(self, nd=3):
+ if nd == 3:
+ return np.array(self.copyDataTo()).reshape(4,4)
+ elif nd == 2:
+ m = np.array(self.copyDataTo()).reshape(4,4)
+ m[2] = m[3]
+ m[:,2] = m[:,3]
+ return m[:3,:3]
+ else:
+ raise Exception("Argument 'nd' must be 2 or 3")
+
+ def map(self, obj):
+ """
+ Extends QMatrix4x4.map() to allow mapping (3, ...) arrays of coordinates
+ """
+ if isinstance(obj, np.ndarray) and obj.ndim >= 2 and obj.shape[0] in (2,3):
+ return pg.transformCoordinates(self, obj)
+ else:
+ return QtGui.QMatrix4x4.map(self, obj)
+
+ def inverted(self):
+ inv, b = QtGui.QMatrix4x4.inverted(self)
+ return Transform3D(inv), b
\ No newline at end of file
diff --git a/Vector.py b/Vector.py
index 79da3162..e9c109d8 100644
--- a/Vector.py
+++ b/Vector.py
@@ -1,59 +1,64 @@
-# -*- coding: utf-8 -*-
-"""
-Vector.py - Extension of QVector3D which adds a few missing methods.
-Copyright 2010 Luke Campagnola
-Distributed under MIT/X11 license. See license.txt for more infomation.
-"""
-
-from .Qt import QtGui, QtCore
-import numpy as np
-
-class Vector(QtGui.QVector3D):
- """Extension of QVector3D which adds a few helpful methods."""
-
- def __init__(self, *args):
- if len(args) == 1:
- if isinstance(args[0], QtCore.QSizeF):
- QtGui.QVector3D.__init__(self, float(args[0].width()), float(args[0].height()), 0)
- return
- elif isinstance(args[0], QtCore.QPoint) or isinstance(args[0], QtCore.QPointF):
- QtGui.QVector3D.__init__(self, float(args[0].x()), float(args[0].y()), 0)
- elif hasattr(args[0], '__getitem__'):
- vals = list(args[0])
- if len(vals) == 2:
- vals.append(0)
- if len(vals) != 3:
- raise Exception('Cannot init Vector with sequence of length %d' % len(args[0]))
- QtGui.QVector3D.__init__(self, *vals)
- return
- elif len(args) == 2:
- QtGui.QVector3D.__init__(self, args[0], args[1], 0)
- return
- QtGui.QVector3D.__init__(self, *args)
-
- def __len__(self):
- return 3
-
- #def __reduce__(self):
- #return (Point, (self.x(), self.y()))
-
- def __getitem__(self, i):
- if i == 0:
- return self.x()
- elif i == 1:
- return self.y()
- elif i == 2:
- return self.z()
- else:
- raise IndexError("Point has no index %s" % str(i))
-
- def __setitem__(self, i, x):
- if i == 0:
- return self.setX(x)
- elif i == 1:
- return self.setY(x)
- elif i == 2:
- return self.setZ(x)
- else:
- raise IndexError("Point has no index %s" % str(i))
-
+# -*- coding: utf-8 -*-
+"""
+Vector.py - Extension of QVector3D which adds a few missing methods.
+Copyright 2010 Luke Campagnola
+Distributed under MIT/X11 license. See license.txt for more infomation.
+"""
+
+from .Qt import QtGui, QtCore
+import numpy as np
+
+class Vector(QtGui.QVector3D):
+ """Extension of QVector3D which adds a few helpful methods."""
+
+ def __init__(self, *args):
+ if len(args) == 1:
+ if isinstance(args[0], QtCore.QSizeF):
+ QtGui.QVector3D.__init__(self, float(args[0].width()), float(args[0].height()), 0)
+ return
+ elif isinstance(args[0], QtCore.QPoint) or isinstance(args[0], QtCore.QPointF):
+ QtGui.QVector3D.__init__(self, float(args[0].x()), float(args[0].y()), 0)
+ elif hasattr(args[0], '__getitem__'):
+ vals = list(args[0])
+ if len(vals) == 2:
+ vals.append(0)
+ if len(vals) != 3:
+ raise Exception('Cannot init Vector with sequence of length %d' % len(args[0]))
+ QtGui.QVector3D.__init__(self, *vals)
+ return
+ elif len(args) == 2:
+ QtGui.QVector3D.__init__(self, args[0], args[1], 0)
+ return
+ QtGui.QVector3D.__init__(self, *args)
+
+ def __len__(self):
+ return 3
+
+ #def __reduce__(self):
+ #return (Point, (self.x(), self.y()))
+
+ def __getitem__(self, i):
+ if i == 0:
+ return self.x()
+ elif i == 1:
+ return self.y()
+ elif i == 2:
+ return self.z()
+ else:
+ raise IndexError("Point has no index %s" % str(i))
+
+ def __setitem__(self, i, x):
+ if i == 0:
+ return self.setX(x)
+ elif i == 1:
+ return self.setY(x)
+ elif i == 2:
+ return self.setZ(x)
+ else:
+ raise IndexError("Point has no index %s" % str(i))
+
+ def __iter__(self):
+ yield(self.x())
+ yield(self.y())
+ yield(self.z())
+
\ No newline at end of file
diff --git a/__init__.py b/__init__.py
index bd7c2e76..dbb54ca9 100644
--- a/__init__.py
+++ b/__init__.py
@@ -165,6 +165,7 @@ from .WidgetGroup import *
from .Point import Point
from .Vector import Vector
from .SRTTransform import SRTTransform
+from .Transform3D import Transform3D
from .SRTTransform3D import SRTTransform3D
from .functions import *
from .graphicsWindows import *
diff --git a/dockarea/DockArea.py b/dockarea/DockArea.py
index d49f02ad..78d512f3 100644
--- a/dockarea/DockArea.py
+++ b/dockarea/DockArea.py
@@ -208,6 +208,10 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
def restoreState(self, state):
"""
Restore Dock configuration as generated by saveState.
+
+ Note that this function does not create any Docks--it will only
+ restore the arrangement of an existing set of Docks.
+
"""
## 1) make dict of all docks and list of existing containers
@@ -240,8 +244,11 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
typ, contents, state = state
pfx = " " * depth
if typ == 'dock':
- obj = docks[contents]
- del docks[contents]
+ try:
+ obj = docks[contents]
+ del docks[contents]
+ except KeyError:
+ raise Exception('Cannot restore dock state; no dock with name "%s"' % contents)
else:
obj = self.makeContainer(typ)
@@ -270,7 +277,7 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
if isinstance(obj, Dock):
d[obj.name()] = obj
- else:
+ elif obj is not None:
c.append(obj)
for i in range(obj.count()):
o2 = obj.widget(i)
diff --git a/examples/GLScatterPlotItem.py b/examples/GLScatterPlotItem.py
index 16033520..e73eacd9 100644
--- a/examples/GLScatterPlotItem.py
+++ b/examples/GLScatterPlotItem.py
@@ -15,48 +15,91 @@ w.show()
g = gl.GLGridItem()
w.addItem(g)
-#pos = np.empty((53, 3))
-#size = np.empty((53))
-#color = np.empty((53, 4))
-#pos[0] = (1,0,0); size[0] = 0.5; color[0] = (1.0, 0.0, 0.0, 0.5)
-#pos[1] = (0,1,0); size[1] = 0.2; color[1] = (0.0, 0.0, 1.0, 0.5)
-#pos[2] = (0,0,1); size[2] = 2./3.; color[2] = (0.0, 1.0, 0.0, 0.5)
-#z = 0.5
-#d = 6.0
-#for i in range(3,53):
- #pos[i] = (0,0,z)
- #size[i] = 2./d
- #color[i] = (0.0, 1.0, 0.0, 0.5)
- #z *= 0.5
- #d *= 2.0
+##
+## First example is a set of points with pxMode=False
+## These demonstrate the ability to have points with real size down to a very small scale
+##
+pos = np.empty((53, 3))
+size = np.empty((53))
+color = np.empty((53, 4))
+pos[0] = (1,0,0); size[0] = 0.5; color[0] = (1.0, 0.0, 0.0, 0.5)
+pos[1] = (0,1,0); size[1] = 0.2; color[1] = (0.0, 0.0, 1.0, 0.5)
+pos[2] = (0,0,1); size[2] = 2./3.; color[2] = (0.0, 1.0, 0.0, 0.5)
+
+z = 0.5
+d = 6.0
+for i in range(3,53):
+ pos[i] = (0,0,z)
+ size[i] = 2./d
+ color[i] = (0.0, 1.0, 0.0, 0.5)
+ z *= 0.5
+ d *= 2.0
-#sp = gl.GLScatterPlotItem(pos=pos, sizes=size, colors=color, pxMode=False)
+sp1 = gl.GLScatterPlotItem(pos=pos, size=size, color=color, pxMode=False)
+sp1.translate(5,5,0)
+w.addItem(sp1)
-pos = (np.random.random(size=(100000,3)) * 10) - 5
+##
+## Second example shows a volume of points with rapidly updating color
+## and pxMode=True
+##
+
+pos = np.random.random(size=(100000,3))
+pos *= [10,-10,10]
+pos[0] = (0,0,0)
color = np.ones((pos.shape[0], 4))
-d = (pos**2).sum(axis=1)**0.5
-color[:,3] = np.clip(-np.cos(d*2) * 0.2, 0, 1)
-sp = gl.GLScatterPlotItem(pos=pos, color=color, size=5)
+d2 = (pos**2).sum(axis=1)**0.5
+size = np.random.random(size=pos.shape[0])*10
+sp2 = gl.GLScatterPlotItem(pos=pos, color=(1,1,1,1), size=size)
phase = 0.
+w.addItem(sp2)
+
+
+##
+## Third example shows a grid of points with rapidly updating position
+## and pxMode = False
+##
+
+pos3 = np.zeros((100,100,3))
+pos3[:,:,:2] = np.mgrid[:100, :100].transpose(1,2,0) * [-0.1,0.1]
+pos3 = pos3.reshape(10000,3)
+d3 = (pos3**2).sum(axis=1)**0.5
+
+sp3 = gl.GLScatterPlotItem(pos=pos3, color=(1,1,1,.3), size=0.1, pxMode=False)
+
+w.addItem(sp3)
+
+
def update():
- global phase, color, sp, d
- s = -np.cos(d*2+phase)
- color[:,3] = np.clip(s * 0.2, 0, 1)
+ ## update volume colors
+ global phase, sp2, d2
+ s = -np.cos(d2*2+phase)
+ color = np.empty((len(d2),4), dtype=np.float32)
+ color[:,3] = np.clip(s * 0.1, 0, 1)
color[:,0] = np.clip(s * 3.0, 0, 1)
color[:,1] = np.clip(s * 1.0, 0, 1)
color[:,2] = np.clip(s ** 3, 0, 1)
-
- sp.setData(color=color)
+ sp2.setData(color=color)
phase -= 0.1
+ ## update surface positions and colors
+ global sp3, d3, pos3
+ z = -np.cos(d3*2+phase)
+ pos3[:,2] = z
+ color = np.empty((len(d3),4), dtype=np.float32)
+ color[:,3] = 0.3
+ color[:,0] = np.clip(z * 3.0, 0, 1)
+ color[:,1] = np.clip(z * 1.0, 0, 1)
+ color[:,2] = np.clip(z ** 3, 0, 1)
+ sp3.setData(pos=pos3, color=color)
+
t = QtCore.QTimer()
t.timeout.connect(update)
t.start(50)
-w.addItem(sp)
## Start Qt event loop unless running in interactive mode.
if sys.flags.interactive != 1:
diff --git a/examples/ROIExamples.py b/examples/ROIExamples.py
index 044e0141..0a436319 100644
--- a/examples/ROIExamples.py
+++ b/examples/ROIExamples.py
@@ -36,12 +36,16 @@ img1a = pg.ImageItem(arr)
v1a.addItem(img1a)
img1b = pg.ImageItem()
v1b.addItem(img1b)
+v1a.disableAutoRange('xy')
+v1b.disableAutoRange('xy')
+v1a.autoRange()
+v1b.autoRange()
rois = []
rois.append(pg.RectROI([20, 20], [20, 20], pen=(0,9)))
rois[-1].addRotateHandle([1,0], [0.5, 0.5])
rois.append(pg.LineROI([0, 60], [20, 80], width=5, pen=(1,9)))
-rois.append(pg.MultiLineROI([[20, 90], [50, 60], [60, 90]], width=5, pen=(2,9)))
+rois.append(pg.MultiRectROI([[20, 90], [50, 60], [60, 90]], width=5, pen=(2,9)))
rois.append(pg.EllipseROI([60, 10], [30, 20], pen=(3,9)))
rois.append(pg.CircleROI([80, 50], [20, 20], pen=(4,9)))
#rois.append(pg.LineSegmentROI([[110, 50], [20, 20]], pen=(5,9)))
@@ -70,6 +74,10 @@ r2a = pg.PolyLineROI([[0,0], [10,10], [10,30], [30,10]], closed=True)
v2a.addItem(r2a)
r2b = pg.PolyLineROI([[0,-20], [10,-10], [10,-30]], closed=False)
v2a.addItem(r2b)
+v2a.disableAutoRange('xy')
+#v2b.disableAutoRange('xy')
+v2a.autoRange()
+#v2b.autoRange()
text = """Building custom ROI types
ROIs can be built with a variety of different handle types
@@ -107,6 +115,9 @@ r3b.addRotateHandle([0, 1], [1, 0])
r3b.addScaleRotateHandle([0, 0.5], [0.5, 0.5])
r3b.addScaleRotateHandle([1, 0.5], [0.5, 0.5])
+v3.disableAutoRange('xy')
+v3.autoRange()
+
text = """Transforming objects with ROI"""
w4 = w.addLayout(row=1, col=1)
@@ -121,6 +132,9 @@ img4 = pg.ImageItem(arr)
v4.addItem(r4)
img4.setParentItem(r4)
+v4.disableAutoRange('xy')
+v4.autoRange()
+
diff --git a/graphicsItems/GradientEditorItem.py b/graphicsItems/GradientEditorItem.py
index 33970d2c..3c078ede 100644
--- a/graphicsItems/GradientEditorItem.py
+++ b/graphicsItems/GradientEditorItem.py
@@ -165,6 +165,9 @@ class TickSliderItem(GraphicsWidget):
tick.setPos(pos)
self.ticks[tick] = float(newX) / self.length
+ def tickMoveFinished(self, tick):
+ pass
+
def tickClicked(self, tick, ev):
if ev.button() == QtCore.Qt.RightButton:
self.removeTick(tick)
@@ -340,16 +343,18 @@ class GradientEditorItem(TickSliderItem):
customizable by the user. :class: `GradientWidget ` provides a widget
with a GradientEditorItem that can be added to a GUI.
- ======================== ===========================================================
+ ================================ ===========================================================
**Signals**
- sigGradientChanged(self) Signal is emitted anytime the gradient changes. The signal
- is emitted in real time while ticks are being dragged or
- colors are being changed.
- ======================== ===========================================================
+ sigGradientChanged(self) Signal is emitted anytime the gradient changes. The signal
+ is emitted in real time while ticks are being dragged or
+ colors are being changed.
+ sigGradientChangeFinished(self) Signal is emitted when the gradient is finished changing.
+ ================================ ===========================================================
"""
sigGradientChanged = QtCore.Signal(object)
+ sigGradientChangeFinished = QtCore.Signal(object)
def __init__(self, *args, **kargs):
"""
@@ -381,6 +386,7 @@ class GradientEditorItem(TickSliderItem):
self.colorDialog.currentColorChanged.connect(self.currentColorChanged)
self.colorDialog.rejected.connect(self.currentColorRejected)
+ self.colorDialog.accepted.connect(self.currentColorAccepted)
self.backgroundRect.setParentItem(self)
self.gradRect.setParentItem(self)
@@ -508,6 +514,9 @@ class GradientEditorItem(TickSliderItem):
self.setTickColor(self.currentTick, self.currentTickColor)
self.updateGradient()
+ def currentColorAccepted(self):
+ self.sigGradientChangeFinished.emit(self)
+
def tickClicked(self, tick, ev):
#private
if ev.button() == QtCore.Qt.LeftButton:
@@ -533,6 +542,9 @@ class GradientEditorItem(TickSliderItem):
TickSliderItem.tickMoved(self, tick, pos)
self.updateGradient()
+ def tickMoveFinished(self, tick):
+ self.sigGradientChangeFinished.emit(self)
+
def getGradient(self):
"""Return a QLinearGradient object."""
@@ -669,7 +681,7 @@ class GradientEditorItem(TickSliderItem):
TickSliderItem.mouseReleaseEvent(self, ev)
self.updateGradient()
- def addTick(self, x, color=None, movable=True):
+ def addTick(self, x, color=None, movable=True, finish=True):
"""
Add a tick to the gradient. Return the tick.
@@ -688,7 +700,17 @@ class GradientEditorItem(TickSliderItem):
t = TickSliderItem.addTick(self, x, color=color, movable=movable)
t.colorChangeAllowed = True
t.removeAllowed = True
+
+ if finish:
+ self.sigGradientChangeFinished.emit(self)
return t
+
+
+ def removeTick(self, tick, finish=True):
+ TickSliderItem.removeTick(self, tick)
+ if finish:
+ self.sigGradientChangeFinished.emit(self)
+
def saveState(self):
"""
@@ -723,13 +745,14 @@ class GradientEditorItem(TickSliderItem):
## public
self.setColorMode(state['mode'])
for t in list(self.ticks.keys()):
- self.removeTick(t)
+ self.removeTick(t, finish=False)
for t in state['ticks']:
c = QtGui.QColor(*t[1])
- self.addTick(t[0], c)
+ self.addTick(t[0], c, finish=False)
self.updateGradient()
+ self.sigGradientChangeFinished.emit(self)
-
+
class Tick(GraphicsObject):
## private class
@@ -791,6 +814,7 @@ class Tick(GraphicsObject):
if ev.isFinish():
self.moving = False
self.sigMoved.emit(self)
+ self.view().tickMoveFinished(self)
def mouseClickEvent(self, ev):
if ev.button() == QtCore.Qt.RightButton and self.moving:
diff --git a/graphicsItems/GraphicsItem.py b/graphicsItems/GraphicsItem.py
index 347d3886..7eee89f3 100644
--- a/graphicsItems/GraphicsItem.py
+++ b/graphicsItems/GraphicsItem.py
@@ -433,4 +433,18 @@ class GraphicsItem(object):
"""
Called whenever the transformation matrix of the view has changed.
"""
- pass
\ No newline at end of file
+ pass
+
+ #def prepareGeometryChange(self):
+ #self._qtBaseClass.prepareGeometryChange(self)
+ #self.informViewBoundsChanged()
+
+ def informViewBoundsChanged(self):
+ """
+ Inform this item's container ViewBox that the bounds of this item have changed.
+ This is used by ViewBox to react if auto-range is enabled.
+ """
+ view = self.getViewBox()
+ if view is not None and hasattr(view, 'implements') and view.implements('ViewBox'):
+ view.itemBoundsChanged(self) ## inform view so it can update its range if it wants
+
\ No newline at end of file
diff --git a/graphicsItems/GraphicsObject.py b/graphicsItems/GraphicsObject.py
index f893d8dc..4361d1e6 100644
--- a/graphicsItems/GraphicsObject.py
+++ b/graphicsItems/GraphicsObject.py
@@ -11,10 +11,13 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject):
_qtBaseClass = QtGui.QGraphicsObject
def __init__(self, *args):
QtGui.QGraphicsObject.__init__(self, *args)
+ self.setFlag(self.ItemSendsGeometryChanges)
GraphicsItem.__init__(self)
def itemChange(self, change, value):
ret = QtGui.QGraphicsObject.itemChange(self, change, value)
if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]:
self._updateView()
+ if change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]:
+ self.informViewBoundsChanged()
return ret
diff --git a/graphicsItems/PlotDataItem.py b/graphicsItems/PlotDataItem.py
index 34af641a..ce5b11aa 100644
--- a/graphicsItems/PlotDataItem.py
+++ b/graphicsItems/PlotDataItem.py
@@ -361,9 +361,11 @@ class PlotDataItem(GraphicsObject):
self.updateItems()
prof.mark('update items')
+
view = self.getViewBox()
if view is not None:
view.itemBoundsChanged(self) ## inform view so it can update its range if it wants
+
self.sigPlotChanged.emit(self)
prof.mark('emit')
prof.finish()
diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py
index 46bc4d5b..3177a176 100644
--- a/graphicsItems/PlotItem/PlotItem.py
+++ b/graphicsItems/PlotItem/PlotItem.py
@@ -506,12 +506,14 @@ class PlotItem(GraphicsWidget):
self.curves.append(item)
#self.addItem(c)
+ if hasattr(item, 'setLogMode'):
+ item.setLogMode(self.ctrl.logXCheck.isChecked(), self.ctrl.logYCheck.isChecked())
+
if isinstance(item, PlotDataItem):
## configure curve for this plot
(alpha, auto) = self.alphaState()
item.setAlpha(alpha, auto)
item.setFftMode(self.ctrl.fftCheck.isChecked())
- item.setLogMode(self.ctrl.logXCheck.isChecked(), self.ctrl.logYCheck.isChecked())
item.setDownsampling(self.downsampleMode())
item.setPointMode(self.pointMode())
@@ -526,6 +528,7 @@ class PlotItem(GraphicsWidget):
#c.connect(c, QtCore.SIGNAL('plotChanged'), self.plotChanged)
#item.sigPlotChanged.connect(self.plotChanged)
#self.plotChanged()
+
def addDataItem(self, item, *args):
print("PlotItem.addDataItem is deprecated. Use addItem instead.")
@@ -878,8 +881,9 @@ class PlotItem(GraphicsWidget):
def updateLogMode(self):
x = self.ctrl.logXCheck.isChecked()
y = self.ctrl.logYCheck.isChecked()
- for c in self.curves:
- c.setLogMode(x,y)
+ for i in self.items:
+ if hasattr(i, 'setLogMode'):
+ i.setLogMode(x,y)
self.getAxis('bottom').setLogMode(x)
self.getAxis('top').setLogMode(x)
self.getAxis('left').setLogMode(y)
diff --git a/graphicsItems/ROI.py b/graphicsItems/ROI.py
index 78ac1ad1..e3f094ff 100644
--- a/graphicsItems/ROI.py
+++ b/graphicsItems/ROI.py
@@ -28,7 +28,7 @@ from .UIGraphicsItem import UIGraphicsItem
__all__ = [
'ROI',
'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'PolygonROI',
- 'LineROI', 'MultiLineROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI',
+ 'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI',
]
@@ -370,7 +370,9 @@ class ROI(GraphicsObject):
else:
return (self.handles[index]['name'], self.handles[index]['item'].scenePos())
-
+ def getHandles(self):
+ return [h['item'] for h in self.handles]
+
def mapSceneToParent(self, pt):
return self.mapToParent(self.mapFromScene(pt))
@@ -538,19 +540,27 @@ class ROI(GraphicsObject):
return True
- def movePoint(self, handle, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish=True):
+ def movePoint(self, handle, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish=True, coords='parent'):
## called by Handles when they are moved.
## pos is the new position of the handle in scene coords, as requested by the handle.
newState = self.stateCopy()
index = self.indexOfHandle(handle)
h = self.handles[index]
- p0 = self.mapToScene(h['pos'] * self.state['size'])
+ p0 = self.mapToParent(h['pos'] * self.state['size'])
p1 = Point(pos)
+ if coords == 'parent':
+ pass
+ elif coords == 'scene':
+ p1 = self.mapSceneToParent(p1)
+ else:
+ raise Exception("New point location must be given in either 'parent' or 'scene' coordinates.")
+
+
## transform p0 and p1 into parent's coordinates (same as scene coords if there is no parent). I forget why.
- p0 = self.mapSceneToParent(p0)
- p1 = self.mapSceneToParent(p1)
+ #p0 = self.mapSceneToParent(p0)
+ #p1 = self.mapSceneToParent(p1)
## Handles with a 'center' need to know their local position relative to the center point (lp0, lp1)
if 'center' in h:
@@ -566,8 +576,8 @@ class ROI(GraphicsObject):
self.translate(p1-p0, snap=snap, update=False)
elif h['type'] == 'f':
- newPos = self.mapFromScene(pos)
- h['item'].setPos(self.mapFromScene(pos))
+ newPos = self.mapFromParent(p1)
+ h['item'].setPos(newPos)
h['pos'] = newPos
self.freeHandleMoved = True
#self.sigRegionChanged.emit(self) ## should be taken care of by call to stateChanged()
@@ -1212,7 +1222,7 @@ class Handle(UIGraphicsItem):
#print "point moved; inform %d ROIs" % len(self.roi)
# A handle can be used by multiple ROIs; tell each to update its handle position
for r in self.rois:
- r.movePoint(self, pos, modifiers, finish=finish)
+ r.movePoint(self, pos, modifiers, finish=finish, coords='scene')
def buildPath(self):
size = self.radius
@@ -1264,9 +1274,9 @@ class Handle(UIGraphicsItem):
if self._shape is None:
s = self.generateShape()
if s is None:
- return self.shape
+ return self.path
self._shape = s
- self.prepareGeometryChange()
+ self.prepareGeometryChange() ## beware--this can cause the view to adjust, which would immediately invalidate the shape.
return self._shape
def boundingRect(self):
@@ -1357,9 +1367,16 @@ class LineROI(ROI):
self.addScaleRotateHandle([1, 0.5], [0, 0.5])
self.addScaleHandle([0.5, 1], [0.5, 0.5])
+
-class MultiLineROI(QtGui.QGraphicsObject):
+class MultiRectROI(QtGui.QGraphicsObject):
+ """
+ Chain of rectangular ROIs connected by handles.
+ This is generally used to mark a curved path through
+ an image similarly to PolyLineROI. It differs in that each segment
+ of the chain is rectangular instead of linear and thus has width.
+ """
sigRegionChangeFinished = QtCore.Signal(object)
sigRegionChangeStarted = QtCore.Signal(object)
sigRegionChanged = QtCore.Signal(object)
@@ -1368,27 +1385,17 @@ class MultiLineROI(QtGui.QGraphicsObject):
QtGui.QGraphicsObject.__init__(self)
self.pen = pen
self.roiArgs = args
+ self.lines = []
if len(points) < 2:
raise Exception("Must start with at least 2 points")
- self.lines = []
- self.lines.append(ROI([0, 0], [1, 5], parent=self, pen=pen, **args))
- self.lines[-1].addScaleHandle([0.5, 1], [0.5, 0.5])
- h = self.lines[-1].addScaleRotateHandle([0, 0.5], [1, 0.5])
- h.movePoint(points[0])
- h.movePoint(points[0])
- for i in range(1, len(points)):
- h = self.lines[-1].addScaleRotateHandle([1, 0.5], [0, 0.5])
- if i < len(points)-1:
- self.lines.append(ROI([0, 0], [1, 5], parent=self, pen=pen, **args))
- self.lines[-1].addScaleRotateHandle([0, 0.5], [1, 0.5], item=h)
- h.movePoint(points[i])
- h.movePoint(points[i])
-
- for l in self.lines:
- l.translatable = False
- l.sigRegionChanged.connect(self.roiChangedEvent)
- l.sigRegionChangeStarted.connect(self.roiChangeStartedEvent)
- l.sigRegionChangeFinished.connect(self.roiChangeFinishedEvent)
+
+ ## create first segment
+ self.addSegment(points[1], connectTo=points[0], scaleHandle=True)
+
+ ## create remaining segments
+ for p in points[2:]:
+ self.addSegment(p)
+
def paint(self, *args):
pass
@@ -1411,7 +1418,13 @@ class MultiLineROI(QtGui.QGraphicsObject):
def roiChangeFinishedEvent(self):
self.sigRegionChangeFinished.emit(self)
-
+ def getHandlePositions(self):
+ """Return the positions of all handles in local coordinates."""
+ pos = [self.mapFromScene(self.lines[0].getHandles()[0].scenePos())]
+ for l in self.lines:
+ pos.append(self.mapFromScene(l.getHandles()[1].scenePos()))
+ return pos
+
def getArrayRegion(self, arr, img=None, axes=(0,1)):
rgns = []
for l in self.lines:
@@ -1432,6 +1445,59 @@ class MultiLineROI(QtGui.QGraphicsObject):
return np.concatenate(rgns, axis=axes[0])
+ def addSegment(self, pos=(0,0), scaleHandle=False, connectTo=None):
+ """
+ Add a new segment to the ROI connecting from the previous endpoint to *pos*.
+ (pos is specified in the parent coordinate system of the MultiRectROI)
+ """
+
+ ## by default, connect to the previous endpoint
+ if connectTo is None:
+ connectTo = self.lines[-1].getHandles()[1]
+
+ ## create new ROI
+ newRoi = ROI((0,0), [1, 5], parent=self, pen=self.pen, **self.roiArgs)
+ self.lines.append(newRoi)
+
+ ## Add first SR handle
+ if isinstance(connectTo, Handle):
+ self.lines[-1].addScaleRotateHandle([0, 0.5], [1, 0.5], item=connectTo)
+ newRoi.movePoint(connectTo, connectTo.scenePos(), coords='scene')
+ else:
+ h = self.lines[-1].addScaleRotateHandle([0, 0.5], [1, 0.5])
+ newRoi.movePoint(h, connectTo, coords='scene')
+
+ ## add second SR handle
+ h = self.lines[-1].addScaleRotateHandle([1, 0.5], [0, 0.5])
+ newRoi.movePoint(h, pos)
+
+ ## optionally add scale handle (this MUST come after the two SR handles)
+ if scaleHandle:
+ newRoi.addScaleHandle([0.5, 1], [0.5, 0.5])
+
+ newRoi.translatable = False
+ newRoi.sigRegionChanged.connect(self.roiChangedEvent)
+ newRoi.sigRegionChangeStarted.connect(self.roiChangeStartedEvent)
+ newRoi.sigRegionChangeFinished.connect(self.roiChangeFinishedEvent)
+ self.sigRegionChanged.emit(self)
+
+
+ def removeSegment(self, index=-1):
+ """Remove a segment from the ROI."""
+ roi = self.lines[index]
+ self.lines.pop(index)
+ self.scene().removeItem(roi)
+ roi.sigRegionChanged.disconnect(self.roiChangedEvent)
+ roi.sigRegionChangeStarted.disconnect(self.roiChangeStartedEvent)
+ roi.sigRegionChangeFinished.disconnect(self.roiChangeFinishedEvent)
+
+ self.sigRegionChanged.emit(self)
+
+
+class MultiLineROI(MultiRectROI):
+ def __init__(self, *args, **kwds):
+ MultiRectROI.__init__(self, *args, **kwds)
+ print("Warning: MultiLineROI has been renamed to MultiRectROI. (and MultiLineROI may be redefined in the future)")
class EllipseROI(ROI):
def __init__(self, pos, size, **args):
@@ -1475,6 +1541,8 @@ class CircleROI(EllipseROI):
self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5])
class PolygonROI(ROI):
+ ## deprecated. Use PloyLineROI instead.
+
def __init__(self, positions, pos=None, **args):
if pos is None:
pos = [0,0]
@@ -1483,16 +1551,17 @@ class PolygonROI(ROI):
for p in positions:
self.addFreeHandle(p)
self.setZValue(1000)
+ print("Warning: PolygonROI is deprecated. Use PolyLineROI instead.")
def listPoints(self):
return [p['item'].pos() for p in self.handles]
- def movePoint(self, *args, **kargs):
- ROI.movePoint(self, *args, **kargs)
- self.prepareGeometryChange()
- for h in self.handles:
- h['pos'] = h['item'].pos()
+ #def movePoint(self, *args, **kargs):
+ #ROI.movePoint(self, *args, **kargs)
+ #self.prepareGeometryChange()
+ #for h in self.handles:
+ #h['pos'] = h['item'].pos()
def paint(self, p, *args):
p.setRenderHint(QtGui.QPainter.Antialiasing)
@@ -1687,103 +1756,33 @@ class LineSegmentROI(ROI):
ROI.__init__(self, pos, [1,1], **args)
#ROI.__init__(self, positions[0])
if len(positions) > 2:
- raise Exception("LineSegmentROI can only be defined by 2 positions. This is an API change.")
+ raise Exception("LineSegmentROI must be defined by exactly 2 positions. For more points, use PolyLineROI.")
for i, p in enumerate(positions):
self.addFreeHandle(p, item=handles[i])
- #self.setZValue(1000)
- #self.parentROI = None
- #self.hasParentROI = False
- #self.setAcceptsHandles(acceptsHandles)
-
- #def setParentROI(self, parent):
- #self.parentROI = parent
- #if parent != None:
- #self.hasParentROI = True
- #else:
- #self.hasParentROI = False
-
- #def setAcceptsHandles(self, b):
- #if b:
- #self.setAcceptedMouseButtons(QtCore.Qt.LeftButton)
- #else:
- #self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
-
- #def close(self):
- ##for h in self.handles:
- ##if len(h['item'].roi) == 1:
- ##h['item'].scene().removeItem(h['item'])
- ##elif h['item'].parentItem() == self:
- ##h['item'].setParentItem(self.parentItem())
-
- #self.scene().removeItem(self)
-
- #def handleRemoved(self, handle):
- #self.parentROI.handleRemoved(self, handle)
-
- #def hoverEvent(self, ev):
- #if (self.translatable or self.acceptsHandles) and (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton):
- ##print " setHover: True"
- #self.setMouseHover(True)
- #self.sigHoverEvent.emit(self)
- #else:
- ##print " setHover: False"
- #self.setMouseHover(False)
-
- #def mouseClickEvent(self, ev):
- #ROI.mouseClickEvent(self, ev) ## only checks for Right-clicks (for now anyway)
- #if ev.button() == QtCore.Qt.LeftButton:
- #if self.acceptsHandles:
- #ev.accept()
- #self.newHandleRequested(ev.pos()) ## ev.pos is the position in this item's coordinates
- #else:
- #ev.ignore()
-
- #def newHandleRequested(self, evPos):
- #print "newHandleRequested"
-
- #if evPos - self.handles[0].pos() == Point(0.,0.) or evPos-handles[1].pos() == Point(0.,0.):
- # return
- #self.parentROI.newHandleRequested(self, self.mapToParent(evPos)) ## so now evPos should be passed in in the parents coordinate system
def listPoints(self):
return [p['item'].pos() for p in self.handles]
- #def movePoint(self, *args, **kargs):
- #ROI.movePoint(self, *args, **kargs)
- #self.prepareGeometryChange()
- #for h in self.handles:
- #h['pos'] = h['item'].pos()
-
def paint(self, p, *args):
p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(self.currentPen)
h1 = self.handles[0]['item'].pos()
h2 = self.handles[1]['item'].pos()
p.drawLine(h1, h2)
- #p.setPen(fn.mkPen('w'))
- #p.drawPath(self.shape())
-
- #for i in range(len(self.handles)-1):
- #h1 = self.handles[i]['item'].pos()
- #h2 = self.handles[i+1]['item'].pos()
- #p.drawLine(h1, h2)
def boundingRect(self):
return self.shape().boundingRect()
- #r = QtCore.QRectF()
- #for h in self.handles:
- #r |= self.mapFromItem(h['item'], h['item'].boundingRect()).boundingRect() ## |= gives the union of the two QRectFs
- #return r
def shape(self):
p = QtGui.QPainterPath()
- #pw, ph = self.pixelSize()
- #pHyp = 4 * (pw**2 + ph**2)**0.5
h1 = self.handles[0]['item'].pos()
h2 = self.handles[1]['item'].pos()
+ dh = h2-h1
+ if dh.length() == 0:
+ return p
pxv = self.pixelVectors(h2-h1)[1]
if pxv is None:
@@ -1799,14 +1798,6 @@ class LineSegmentROI(ROI):
return p
- #def stateCopy(self):
- #sc = {}
- #sc['pos'] = Point(self.state['pos'])
- #sc['size'] = Point(self.state['size'])
- #sc['angle'] = self.state['angle']
- ##sc['handles'] = self.handles
- #return sc
-
def getArrayRegion(self, data, img, axes=(0,1)):
"""
Use the position of this ROI relative to an imageItem to pull a slice from an array.
@@ -1849,11 +1840,11 @@ class SpiralROI(ROI):
return QtCore.QRectF(-r*1.1, -r*1.1, 2.2*r, 2.2*r)
#return self.bounds
- def movePoint(self, *args, **kargs):
- ROI.movePoint(self, *args, **kargs)
- self.prepareGeometryChange()
- for h in self.handles:
- h['pos'] = h['item'].pos()/self.state['size'][0]
+ #def movePoint(self, *args, **kargs):
+ #ROI.movePoint(self, *args, **kargs)
+ #self.prepareGeometryChange()
+ #for h in self.handles:
+ #h['pos'] = h['item'].pos()/self.state['size'][0]
def stateChanged(self):
ROI.stateChanged(self)
diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py
index d403291b..89fa1487 100644
--- a/graphicsItems/ViewBox/ViewBox.py
+++ b/graphicsItems/ViewBox/ViewBox.py
@@ -109,6 +109,7 @@ class ViewBox(GraphicsWidget):
'background': None,
}
+ self._updatingRange = False ## Used to break recursive loops. See updateAutoRange.
self.setFlag(self.ItemClipsChildrenToShape)
@@ -340,6 +341,7 @@ class ViewBox(GraphicsWidget):
============= =====================================================================
"""
+
changes = {}
if rect is not None:
@@ -471,6 +473,7 @@ class ViewBox(GraphicsWidget):
#if not enable:
#import traceback
#traceback.print_stack()
+
if enable is True:
enable = 1.0
@@ -520,74 +523,84 @@ class ViewBox(GraphicsWidget):
self.updateAutoRange()
def updateAutoRange(self):
- targetRect = self.viewRange()
- if not any(self.state['autoRange']):
+ ## Break recursive loops when auto-ranging.
+ ## This is needed because some items change their size in response
+ ## to a view change.
+ if self._updatingRange:
return
-
- fractionVisible = self.state['autoRange'][:]
- for i in [0,1]:
- if type(fractionVisible[i]) is bool:
- fractionVisible[i] = 1.0
-
- childRange = None
-
- order = [0,1]
- if self.state['autoVisibleOnly'][0] is True:
- order = [1,0]
-
- args = {}
- for ax in order:
- if self.state['autoRange'][ax] is False:
- continue
- if self.state['autoVisibleOnly'][ax]:
- oRange = [None, None]
- oRange[ax] = targetRect[1-ax]
- childRange = self.childrenBounds(frac=fractionVisible, orthoRange=oRange)
+
+ self._updatingRange = True
+ try:
+ targetRect = self.viewRange()
+ if not any(self.state['autoRange']):
+ return
- else:
- if childRange is None:
- childRange = self.childrenBounds(frac=fractionVisible)
-
- ## Make corrections to range
- xr = childRange[ax]
- if xr is not None:
- if self.state['autoPan'][ax]:
- x = sum(xr) * 0.5
- #x = childRect.center().x()
- w2 = (targetRect[ax][1]-targetRect[ax][0]) / 2.
- #childRect.setLeft(x-w2)
- #childRect.setRight(x+w2)
- childRange[ax] = [x-w2, x+w2]
- else:
- #wp = childRect.width() * 0.02
- wp = (xr[1] - xr[0]) * 0.02
- #childRect = childRect.adjusted(-wp, 0, wp, 0)
- childRange[ax][0] -= wp
- childRange[ax][1] += wp
- #targetRect[ax][0] = childRect.left()
- #targetRect[ax][1] = childRect.right()
- targetRect[ax] = childRange[ax]
- args['xRange' if ax == 0 else 'yRange'] = targetRect[ax]
- #else:
- ### Make corrections to Y range
- #if self.state['autoPan'][1]:
- #y = childRect.center().y()
- #h2 = (targetRect[1][1]-targetRect[1][0]) / 2.
- #childRect.setTop(y-h2)
- #childRect.setBottom(y+h2)
- #else:
- #hp = childRect.height() * 0.02
- #childRect = childRect.adjusted(0, -hp, 0, hp)
+ fractionVisible = self.state['autoRange'][:]
+ for i in [0,1]:
+ if type(fractionVisible[i]) is bool:
+ fractionVisible[i] = 1.0
+
+ childRange = None
+
+ order = [0,1]
+ if self.state['autoVisibleOnly'][0] is True:
+ order = [1,0]
+
+ args = {}
+ for ax in order:
+ if self.state['autoRange'][ax] is False:
+ continue
+ if self.state['autoVisibleOnly'][ax]:
+ oRange = [None, None]
+ oRange[ax] = targetRect[1-ax]
+ childRange = self.childrenBounds(frac=fractionVisible, orthoRange=oRange)
- #targetRect[1][0] = childRect.top()
- #targetRect[1][1] = childRect.bottom()
- #args['yRange'] = targetRect[1]
- if len(args) == 0:
- return
- args['padding'] = 0
- args['disableAutoRange'] = False
- #self.setRange(xRange=targetRect[0], yRange=targetRect[1], padding=0, disableAutoRange=False)
- self.setRange(**args)
+ else:
+ if childRange is None:
+ childRange = self.childrenBounds(frac=fractionVisible)
+
+ ## Make corrections to range
+ xr = childRange[ax]
+ if xr is not None:
+ if self.state['autoPan'][ax]:
+ x = sum(xr) * 0.5
+ #x = childRect.center().x()
+ w2 = (targetRect[ax][1]-targetRect[ax][0]) / 2.
+ #childRect.setLeft(x-w2)
+ #childRect.setRight(x+w2)
+ childRange[ax] = [x-w2, x+w2]
+ else:
+ #wp = childRect.width() * 0.02
+ wp = (xr[1] - xr[0]) * 0.02
+ #childRect = childRect.adjusted(-wp, 0, wp, 0)
+ childRange[ax][0] -= wp
+ childRange[ax][1] += wp
+ #targetRect[ax][0] = childRect.left()
+ #targetRect[ax][1] = childRect.right()
+ targetRect[ax] = childRange[ax]
+ args['xRange' if ax == 0 else 'yRange'] = targetRect[ax]
+ #else:
+ ### Make corrections to Y range
+ #if self.state['autoPan'][1]:
+ #y = childRect.center().y()
+ #h2 = (targetRect[1][1]-targetRect[1][0]) / 2.
+ #childRect.setTop(y-h2)
+ #childRect.setBottom(y+h2)
+ #else:
+ #hp = childRect.height() * 0.02
+ #childRect = childRect.adjusted(0, -hp, 0, hp)
+
+ #targetRect[1][0] = childRect.top()
+ #targetRect[1][1] = childRect.bottom()
+ #args['yRange'] = targetRect[1]
+ if len(args) == 0:
+ return
+ args['padding'] = 0
+ args['disableAutoRange'] = False
+ #self.setRange(xRange=targetRect[0], yRange=targetRect[1], padding=0, disableAutoRange=False)
+ self.setRange(**args)
+ finally:
+ self._updatingRange = False
def setXLink(self, view):
"""Link this view's X axis to another view. (see LinkView)"""
diff --git a/opengl/GLGraphicsItem.py b/opengl/GLGraphicsItem.py
index 96cc6763..7d1cf70b 100644
--- a/opengl/GLGraphicsItem.py
+++ b/opengl/GLGraphicsItem.py
@@ -1,4 +1,5 @@
from pyqtgraph.Qt import QtGui, QtCore
+from pyqtgraph import Transform3D
class GLGraphicsItem(QtCore.QObject):
def __init__(self, parentItem=None):
@@ -6,7 +7,7 @@ class GLGraphicsItem(QtCore.QObject):
self.__parent = None
self.__view = None
self.__children = set()
- self.__transform = QtGui.QMatrix4x4()
+ self.__transform = Transform3D()
self.__visible = True
self.setParentItem(parentItem)
self.setDepthValue(0)
@@ -50,7 +51,7 @@ class GLGraphicsItem(QtCore.QObject):
return self.__depthValue
def setTransform(self, tr):
- self.__transform = tr
+ self.__transform = Transform3D(tr)
self.update()
def resetTransform(self):
@@ -73,12 +74,22 @@ class GLGraphicsItem(QtCore.QObject):
def transform(self):
return self.__transform
+ def viewTransform(self):
+ tr = self.__transform
+ p = self
+ while True:
+ p = p.parentItem()
+ if p is None:
+ break
+ tr = p.transform() * tr
+ return Transform3D(tr)
+
def translate(self, dx, dy, dz, local=False):
"""
Translate the object by (*dx*, *dy*, *dz*) in its parent's coordinate system.
If *local* is True, then translation takes place in local coordinates.
"""
- tr = QtGui.QMatrix4x4()
+ tr = Transform3D()
tr.translate(dx, dy, dz)
self.applyTransform(tr, local=local)
@@ -88,7 +99,7 @@ class GLGraphicsItem(QtCore.QObject):
*angle* is in degrees.
"""
- tr = QtGui.QMatrix4x4()
+ tr = Transform3D()
tr.rotate(angle, x, y, z)
self.applyTransform(tr, local=local)
@@ -97,7 +108,7 @@ class GLGraphicsItem(QtCore.QObject):
Scale the object by (*dx*, *dy*, *dz*) in its local coordinate system.
If *local* is False, then scale takes place in the parent's coordinates.
"""
- tr = QtGui.QMatrix4x4()
+ tr = Transform3D()
tr.scale(x, y, z)
self.applyTransform(tr, local=local)
@@ -138,8 +149,29 @@ class GLGraphicsItem(QtCore.QObject):
return
v.updateGL()
+ def mapToParent(self, point):
+ tr = self.transform()
+ if tr is None:
+ return point
+ return tr.map(point)
+
def mapFromParent(self, point):
tr = self.transform()
if tr is None:
return point
- return tr.inverted()[0].map(point)
\ No newline at end of file
+ return tr.inverted()[0].map(point)
+
+ def mapToView(self, point):
+ tr = self.viewTransform()
+ if tr is None:
+ return point
+ return tr.map(point)
+
+ def mapFromView(self, point):
+ tr = self.viewTransform()
+ if tr is None:
+ return point
+ return tr.inverted()[0].map(point)
+
+
+
\ No newline at end of file
diff --git a/opengl/GLViewWidget.py b/opengl/GLViewWidget.py
index 3e105491..6911d849 100644
--- a/opengl/GLViewWidget.py
+++ b/opengl/GLViewWidget.py
@@ -1,8 +1,8 @@
from pyqtgraph.Qt import QtCore, QtGui, QtOpenGL
from OpenGL.GL import *
import numpy as np
-
-Vector = QtGui.QVector3D
+from pyqtgraph import Vector
+##Vector = QtGui.QVector3D
class GLViewWidget(QtOpenGL.QGLWidget):
"""
@@ -181,10 +181,14 @@ class GLViewWidget(QtOpenGL.QGLWidget):
def pixelSize(self, pos):
"""
Return the approximate size of a screen pixel at the location pos
-
+ Pos may be a Vector or an (N,3) array of locations
"""
cam = self.cameraPosition()
- dist = (pos-cam).length()
+ if isinstance(pos, np.ndarray) and pos.ndim == 2:
+ cam = np.array(cam).reshape(1,3)
+ dist = ((pos-cam)**2).sum(axis=1)**0.5
+ else:
+ dist = (pos-cam).length()
xDist = dist * 2. * np.tan(0.5 * self.opts['fov'] * np.pi / 180.)
return xDist / self.width()
diff --git a/opengl/items/GLMeshItem.py b/opengl/items/GLMeshItem.py
index 790c6760..266b84c0 100644
--- a/opengl/items/GLMeshItem.py
+++ b/opengl/items/GLMeshItem.py
@@ -28,7 +28,7 @@ class GLMeshItem(GLGraphicsItem):
GLGraphicsItem.__init__(self)
def initializeGL(self):
- self.shader = shaders.getShader('balloon')
+ self.shader = shaders.getShaderProgram('balloon')
l = glGenLists(1)
self.triList = l
@@ -72,7 +72,9 @@ class GLMeshItem(GLGraphicsItem):
def paint(self):
- shaders.glUseProgram(self.shader)
- glCallList(self.triList)
- shaders.glUseProgram(0)
+ with self.shader:
+ glCallList(self.triList)
+ #shaders.glUseProgram(self.shader)
+ #glCallList(self.triList)
+ #shaders.glUseProgram(0)
#glCallList(self.meshList)
diff --git a/opengl/items/GLScatterPlotItem.py b/opengl/items/GLScatterPlotItem.py
index 3ef3f11b..9ff22c37 100644
--- a/opengl/items/GLScatterPlotItem.py
+++ b/opengl/items/GLScatterPlotItem.py
@@ -1,5 +1,7 @@
from OpenGL.GL import *
+from OpenGL.arrays import vbo
from .. GLGraphicsItem import GLGraphicsItem
+from .. import shaders
from pyqtgraph import QtGui
import numpy as np
@@ -14,6 +16,7 @@ class GLScatterPlotItem(GLGraphicsItem):
self.size = 10
self.color = [1.0,1.0,1.0,0.5]
self.pxMode = True
+ #self.vbo = {} ## VBO does not appear to improve performance very much.
self.setData(**kwds)
def setData(self, **kwds):
@@ -39,13 +42,16 @@ class GLScatterPlotItem(GLGraphicsItem):
for k in kwds.keys():
if k not in args:
raise Exception('Invalid keyword argument: %s (allowed arguments are %s)' % (k, str(args)))
- self.pos = kwds.get('pos', self.pos)
- self.color = kwds.get('color', self.color)
- self.size = kwds.get('size', self.size)
+
+ args.remove('pxMode')
+ for arg in args:
+ if arg in kwds:
+ setattr(self, arg, kwds[arg])
+ #self.vbo.pop(arg, None)
+
self.pxMode = kwds.get('pxMode', self.pxMode)
self.update()
-
def initializeGL(self):
## Generate texture for rendering points
@@ -65,75 +71,108 @@ class GLScatterPlotItem(GLGraphicsItem):
glBindTexture(GL_TEXTURE_2D, self.pointTexture)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, pData.shape[0], pData.shape[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, pData)
- def paint(self):
- glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
+ self.shader = shaders.getShaderProgram('point_sprite')
+
+ #def getVBO(self, name):
+ #if name not in self.vbo:
+ #self.vbo[name] = vbo.VBO(getattr(self, name).astype('f'))
+ #return self.vbo[name]
+
+ def setupGLState(self):
+ """Prepare OpenGL state for drawing. This function is called immediately before painting."""
+ #glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) ## requires z-sorting to render properly.
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE)
glEnable( GL_BLEND )
glEnable( GL_ALPHA_TEST )
- glEnable( GL_POINT_SMOOTH )
+ glDisable( GL_DEPTH_TEST )
+
+ #glEnable( GL_POINT_SMOOTH )
- glHint(GL_POINT_SMOOTH_HINT, GL_NICEST)
+ #glHint(GL_POINT_SMOOTH_HINT, GL_NICEST)
#glPointParameterfv(GL_POINT_DISTANCE_ATTENUATION, (0, 0, -1e-3))
#glPointParameterfv(GL_POINT_SIZE_MAX, (65500,))
#glPointParameterfv(GL_POINT_SIZE_MIN, (0,))
+ def paint(self):
+ self.setupGLState()
+
glEnable(GL_POINT_SPRITE)
+
glActiveTexture(GL_TEXTURE0)
glEnable( GL_TEXTURE_2D )
glBindTexture(GL_TEXTURE_2D, self.pointTexture)
glTexEnvi(GL_POINT_SPRITE, GL_COORD_REPLACE, GL_TRUE)
#glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE) ## use texture color exactly
- glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE ) ## texture modulates current color
+ #glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE ) ## texture modulates current color
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
+ glEnable(GL_PROGRAM_POINT_SIZE)
- if self.pxMode:
- glVertexPointerf(self.pos)
- if isinstance(self.color, np.ndarray):
- glColorPointerf(self.color)
- else:
- if isinstance(self.color, QtGui.QColor):
- glColor4f(*fn.glColor(self.color))
- else:
- glColor4f(*self.color)
- if isinstance(self.size, np.ndarray):
- raise Exception('Array size not yet supported in pxMode (hopefully soon)')
-
- glPointSize(self.size)
+ with self.shader:
+ #glUniform1i(self.shader.uniform('texture'), 0) ## inform the shader which texture to use
glEnableClientState(GL_VERTEX_ARRAY)
- glEnableClientState(GL_COLOR_ARRAY)
- glDrawArrays(GL_POINTS, 0, len(self.pos))
- else:
+ try:
+ glVertexPointerf(self.pos)
-
- for i in range(len(self.pos)):
- pos = self.pos[i]
-
if isinstance(self.color, np.ndarray):
- color = self.color[i]
+ glEnableClientState(GL_COLOR_ARRAY)
+ glColorPointerf(self.color)
else:
- color = self.color
- if isinstance(self.color, QtGui.QColor):
- color = fn.glColor(self.color)
-
- if isinstance(self.size, np.ndarray):
- size = self.size[i]
- else:
- size = self.size
-
- pxSize = self.view().pixelSize(QtGui.QVector3D(*pos))
+ if isinstance(self.color, QtGui.QColor):
+ glColor4f(*fn.glColor(self.color))
+ else:
+ glColor4f(*self.color)
- glPointSize(size / pxSize)
- glBegin( GL_POINTS )
- glColor4f(*color) # x is blue
- #glNormal3f(size, 0, 0)
- glVertex3f(*pos)
- glEnd()
+ if not self.pxMode or isinstance(self.size, np.ndarray):
+ glEnableClientState(GL_NORMAL_ARRAY)
+ norm = np.empty(self.pos.shape)
+ if self.pxMode:
+ norm[:,0] = self.size
+ else:
+ gpos = self.mapToView(self.pos.transpose()).transpose()
+ pxSize = self.view().pixelSize(gpos)
+ norm[:,0] = self.size / pxSize
+
+ glNormalPointerf(norm)
+ else:
+ glNormal3f(self.size,0,0)
+ #glPointSize(self.size)
+ glDrawArrays(GL_POINTS, 0, len(self.pos))
+ finally:
+ glDisableClientState(GL_NORMAL_ARRAY)
+ glDisableClientState(GL_VERTEX_ARRAY)
+ glDisableClientState(GL_COLOR_ARRAY)
+ #posVBO.unbind()
+
+ #for i in range(len(self.pos)):
+ #pos = self.pos[i]
+
+ #if isinstance(self.color, np.ndarray):
+ #color = self.color[i]
+ #else:
+ #color = self.color
+ #if isinstance(self.color, QtGui.QColor):
+ #color = fn.glColor(self.color)
+
+ #if isinstance(self.size, np.ndarray):
+ #size = self.size[i]
+ #else:
+ #size = self.size
+
+ #pxSize = self.view().pixelSize(QtGui.QVector3D(*pos))
+
+ #glPointSize(size / pxSize)
+ #glBegin( GL_POINTS )
+ #glColor4f(*color) # x is blue
+ ##glNormal3f(size, 0, 0)
+ #glVertex3f(*pos)
+ #glEnd()
-
\ No newline at end of file
+
diff --git a/opengl/shaders.py b/opengl/shaders.py
index b1216e35..7f4fa665 100644
--- a/opengl/shaders.py
+++ b/opengl/shaders.py
@@ -3,39 +3,107 @@ from OpenGL.GL import shaders
## For centralizing and managing vertex/fragment shader programs.
+def initShaders():
+ global Shaders
+ Shaders = [
+ ShaderProgram('balloon', [ ## increases fragment alpha as the normal turns orthogonal to the view
+ VertexShader("""
+ varying vec3 normal;
+ void main() {
+ normal = normalize(gl_NormalMatrix * gl_Normal);
+ //vec4 color = normal;
+ //normal.w = min(color.w + 2.0 * color.w * pow(normal.x*normal.x + normal.y*normal.y, 2.0), 1.0);
+ gl_FrontColor = gl_Color;
+ gl_BackColor = gl_Color;
+ gl_Position = ftransform();
+ }
+ """),
+ FragmentShader("""
+ varying vec3 normal;
+ void main() {
+ vec4 color = gl_Color;
+ color.w = min(color.w + 2.0 * color.w * pow(normal.x*normal.x + normal.y*normal.y, 5.0), 1.0);
+ gl_FragColor = color;
+ }
+ """)
+ ]),
+ ShaderProgram('point_sprite', [ ## allows specifying point size using normal.x
+ ## See:
+ ##
+ ## http://stackoverflow.com/questions/9609423/applying-part-of-a-texture-sprite-sheet-texture-map-to-a-point-sprite-in-ios
+ ## http://stackoverflow.com/questions/3497068/textured-points-in-opengl-es-2-0
+ ##
+ ##
+ VertexShader("""
+ void main() {
+ gl_FrontColor=gl_Color;
+ gl_PointSize = gl_Normal.x;
+ gl_Position = ftransform();
+ }
+ """),
+ #FragmentShader("""
+ ##version 120
+ #uniform sampler2D texture;
+ #void main ( )
+ #{
+ #gl_FragColor = texture2D(texture, gl_PointCoord) * gl_Color;
+ #}
+ #""")
+ ]),
+ ]
-Shaders = {
- 'balloon': ( ## increases fragment alpha as the normal turns orthogonal to the view
- """
- varying vec3 normal;
- void main() {
- normal = normalize(gl_NormalMatrix * gl_Normal);
- //vec4 color = normal;
- //normal.w = min(color.w + 2.0 * color.w * pow(normal.x*normal.x + normal.y*normal.y, 2.0), 1.0);
- gl_FrontColor = gl_Color;
- gl_BackColor = gl_Color;
- gl_Position = ftransform();
- }
- """,
- """
- varying vec3 normal;
- void main() {
- vec4 color = gl_Color;
- color.w = min(color.w + 2.0 * color.w * pow(normal.x*normal.x + normal.y*normal.y, 5.0), 1.0);
- gl_FragColor = color;
- }
- """
- ),
-}
-CompiledShaders = {}
+
+CompiledShaderPrograms = {}
-def getShader(name):
- global Shaders, CompiledShaders
+def getShaderProgram(name):
+ return ShaderProgram.names[name]
+
+class VertexShader:
+ def __init__(self, code):
+ self.code = code
+ self.compiled = None
+
+ def shader(self):
+ if self.compiled is None:
+ self.compiled = shaders.compileShader(self.code, GL_VERTEX_SHADER)
+ return self.compiled
+
+class FragmentShader:
+ def __init__(self, code):
+ self.code = code
+ self.compiled = None
+
+ def shader(self):
+ if self.compiled is None:
+ self.compiled = shaders.compileShader(self.code, GL_FRAGMENT_SHADER)
+ return self.compiled
+
+
+
+class ShaderProgram:
+ names = {}
- if name not in CompiledShaders:
- vshader, fshader = Shaders[name]
- vcomp = shaders.compileShader(vshader, GL_VERTEX_SHADER)
- fcomp = shaders.compileShader(fshader, GL_FRAGMENT_SHADER)
- prog = shaders.compileProgram(vcomp, fcomp)
- CompiledShaders[name] = prog, vcomp, fcomp
- return CompiledShaders[name][0]
+ def __init__(self, name, shaders):
+ self.name = name
+ ShaderProgram.names[name] = self
+ self.shaders = shaders
+ self.prog = None
+
+ def program(self):
+ if self.prog is None:
+ compiled = [s.shader() for s in self.shaders] ## compile all shaders
+ self.prog = shaders.compileProgram(*compiled) ## compile program
+ return self.prog
+
+ def __enter__(self):
+ glUseProgram(self.program())
+
+ def __exit__(self, *args):
+ glUseProgram(0)
+
+ def uniform(self, name):
+ """Return the location integer for a uniform variable in this program"""
+ return glGetUniformLocation(self.program(), name)
+
+
+initShaders()
\ No newline at end of file
diff --git a/parametertree/parameterTypes.py b/parametertree/parameterTypes.py
index 315081ce..3aab5a6d 100644
--- a/parametertree/parameterTypes.py
+++ b/parametertree/parameterTypes.py
@@ -516,6 +516,7 @@ class ListParameter(Parameter):
if opts.get('limits', None) is None:
opts['limits'] = []
Parameter.__init__(self, **opts)
+ self.setLimits(opts['limits'])
def setLimits(self, limits):
self.forward, self.reverse = self.mapping(limits)
diff --git a/widgets/GradientWidget.py b/widgets/GradientWidget.py
index ecf3f585..2b9b52d2 100644
--- a/widgets/GradientWidget.py
+++ b/widgets/GradientWidget.py
@@ -11,6 +11,7 @@ __all__ = ['TickSlider', 'GradientWidget', 'BlackWhiteSlider']
class GradientWidget(GraphicsView):
sigGradientChanged = QtCore.Signal(object)
+ sigGradientChangeFinished = QtCore.Signal(object)
def __init__(self, parent=None, orientation='bottom', *args, **kargs):
GraphicsView.__init__(self, parent, useOpenGL=False, background=None)
@@ -18,6 +19,7 @@ class GradientWidget(GraphicsView):
kargs['tickPen'] = 'k'
self.item = GradientEditorItem(*args, **kargs)
self.item.sigGradientChanged.connect(self.sigGradientChanged)
+ self.item.sigGradientChangeFinished.connect(self.sigGradientChangeFinished)
self.setCentralItem(self.item)
self.setOrientation(orientation)
self.setCacheMode(self.CacheNone)
diff --git a/widgets/TreeWidget.py b/widgets/TreeWidget.py
index ef2f1ec9..97fbe953 100644
--- a/widgets/TreeWidget.py
+++ b/widgets/TreeWidget.py
@@ -201,8 +201,10 @@ class TreeWidget(QtGui.QTreeWidget):
for item in items:
self.prepareMove(item)
QtGui.QTreeWidget.clear(self)
- for item in items:
- self.informTreeWidgetChange(item)
+
+ ## Why do we want to do this? It causes RuntimeErrors.
+ #for item in items:
+ #self.informTreeWidgetChange(item)
class TreeWidgetItem(QtGui.QTreeWidgetItem):