summaryrefslogtreecommitdiff
path: root/silx/gui/plot/backends
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/backends')
-rw-r--r--silx/gui/plot/backends/BackendBase.py17
-rw-r--r--silx/gui/plot/backends/BackendMatplotlib.py52
-rw-r--r--silx/gui/plot/backends/BackendOpenGL.py236
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotTriangles.py193
-rw-r--r--silx/gui/plot/backends/glutils/GLText.py23
-rw-r--r--silx/gui/plot/backends/glutils/GLTexture.py3
-rw-r--r--silx/gui/plot/backends/glutils/__init__.py3
7 files changed, 413 insertions, 114 deletions
diff --git a/silx/gui/plot/backends/BackendBase.py b/silx/gui/plot/backends/BackendBase.py
index 0514c85..af37543 100644
--- a/silx/gui/plot/backends/BackendBase.py
+++ b/silx/gui/plot/backends/BackendBase.py
@@ -170,6 +170,23 @@ class BackendBase(object):
"""
return legend
+ def addTriangles(self, x, y, triangles, legend,
+ color, z, selectable, alpha):
+ """Add a set of triangles.
+
+ :param numpy.ndarray x: The data corresponding to the x axis
+ :param numpy.ndarray y: The data corresponding to the y axis
+ :param numpy.ndarray triangles: The indices to make triangles
+ as a (Ntriangle, 3) array
+ :param str legend: The legend to be associated to the curve
+ :param numpy.ndarray color: color(s) as (npoints, 4) array
+ :param int z: Layer on which to draw the cuve
+ :param bool selectable: indicate if the curve can be selected
+ :param float alpha: Opacity as a float in [0., 1.]
+ :returns: The triangles' unique identifier used by the backend
+ """
+ return legend
+
def addItem(self, x, y, legend, shape, color, fill, overlay, z,
linestyle, linewidth, linebgcolor):
"""Add an item (i.e. a shape) to the plot.
diff --git a/silx/gui/plot/backends/BackendMatplotlib.py b/silx/gui/plot/backends/BackendMatplotlib.py
index 726a839..7739329 100644
--- a/silx/gui/plot/backends/BackendMatplotlib.py
+++ b/silx/gui/plot/backends/BackendMatplotlib.py
@@ -54,7 +54,8 @@ from matplotlib.backend_bases import MouseEvent
from matplotlib.lines import Line2D
from matplotlib.collections import PathCollection, LineCollection
from matplotlib.ticker import Formatter, ScalarFormatter, Locator
-
+from matplotlib.tri import Triangulation
+from matplotlib.collections import TriMesh
from . import BackendBase
from .._utils import FLOAT32_MINPOS
@@ -359,9 +360,12 @@ class BackendMatplotlib(BackendBase.BackendBase):
else:
errorbarColor = color
- # On Debian 7 at least, Nx1 array yerr does not seems supported
+ # Nx1 error array deprecated in matplotlib >=3.1 (removed in 3.3)
+ if (isinstance(xerror, numpy.ndarray) and xerror.ndim == 2 and
+ xerror.shape[1] == 1):
+ xerror = numpy.ravel(xerror)
if (isinstance(yerror, numpy.ndarray) and yerror.ndim == 2 and
- yerror.shape[1] == 1 and len(x) != 1):
+ yerror.shape[1] == 1):
yerror = numpy.ravel(yerror)
errorbars = axes.errorbar(x, y, label=legend,
@@ -477,6 +481,32 @@ class BackendMatplotlib(BackendBase.BackendBase):
self.ax.add_artist(image)
return image
+ def addTriangles(self, x, y, triangles, legend,
+ color, z, selectable, alpha):
+ for parameter in (x, y, triangles, legend, color,
+ z, selectable, alpha):
+ assert parameter is not None
+
+ # 0 enables picking on filled triangle
+ picker = 0 if selectable else None
+
+ color = numpy.array(color, copy=False)
+ assert color.ndim == 2 and len(color) == len(x)
+
+ if color.dtype not in [numpy.float32, numpy.float]:
+ color = color.astype(numpy.float32) / 255.
+
+ collection = TriMesh(
+ Triangulation(x, y, triangles),
+ label=legend,
+ alpha=alpha,
+ picker=picker,
+ zorder=z)
+ collection.set_color(color)
+ self.ax.add_collection(collection)
+
+ return collection
+
def addItem(self, x, y, legend, shape, color, fill, overlay, z,
linestyle, linewidth, linebgcolor):
if (linebgcolor is not None and
@@ -1100,6 +1130,22 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
elif label.startswith('__IMAGE__'):
self._picked.append({'kind': 'image', 'legend': label[9:]})
+ elif isinstance(event.artist, TriMesh):
+ # Convert selected triangle to data point indices
+ triangulation = event.artist._triangulation
+ indices = triangulation.get_masked_triangles()[event.ind[0]]
+
+ # Sort picked triangle points by distance to mouse
+ # from furthest to closest to put closest point last
+ # This is to be somewhat consistent with last scatter point
+ # being the top one.
+ dists = ((triangulation.x[indices] - event.mouseevent.xdata) ** 2 +
+ (triangulation.y[indices] - event.mouseevent.ydata) ** 2)
+ indices = indices[numpy.flip(numpy.argsort(dists))]
+
+ self._picked.append({'kind': 'curve', 'legend': label,
+ 'indices': indices})
+
else: # it's a curve, item have no picker for now
if not isinstance(event.artist, (PathCollection, Line2D)):
_logger.info('Unsupported artist, ignored')
diff --git a/silx/gui/plot/backends/BackendOpenGL.py b/silx/gui/plot/backends/BackendOpenGL.py
index e33d03c..0420aa9 100644
--- a/silx/gui/plot/backends/BackendOpenGL.py
+++ b/silx/gui/plot/backends/BackendOpenGL.py
@@ -31,8 +31,9 @@ __license__ = "MIT"
__date__ = "21/12/2018"
from collections import OrderedDict, namedtuple
-from ctypes import c_void_p
import logging
+import warnings
+import weakref
import numpy
@@ -44,7 +45,7 @@ from ... import qt
from ..._glutils import gl
from ... import _glutils as glu
from .glutils import (
- GLLines2D,
+ GLLines2D, GLPlotTriangles,
GLPlotCurve2D, GLPlotColormap, GLPlotRGBAImage, GLPlotFrame2D,
mat4Ortho, mat4Identity,
LEFT, RIGHT, BOTTOM, TOP,
@@ -106,7 +107,7 @@ class PlotDataContent(object):
This class is only meant to work with _OpenGLPlotCanvas.
"""
- _PRIMITIVE_TYPES = 'curve', 'image'
+ _PRIMITIVE_TYPES = 'curve', 'image', 'triangles'
def __init__(self):
self._primitives = OrderedDict() # For images and curves
@@ -124,6 +125,8 @@ class PlotDataContent(object):
primitiveType = 'curve'
elif isinstance(primitive, (GLPlotColormap, GLPlotRGBAImage)):
primitiveType = 'image'
+ elif isinstance(primitive, GLPlotTriangles):
+ primitiveType = 'triangles'
else:
raise RuntimeError('Unsupported object type: %s', primitive)
@@ -304,16 +307,8 @@ _texFragShd = """
}
"""
-
# BackendOpenGL ###############################################################
-_current_context = None
-
-
-def _getContext():
- assert _current_context is not None
- return _current_context
-
class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
"""OpenGL-based Plot backend.
@@ -348,7 +343,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
_baseVertShd, _baseFragShd, attrib0='position')
self._progTex = glu.Program(
_texVertShd, _texFragShd, attrib0='position')
- self._plotFBOs = {}
+ self._plotFBOs = weakref.WeakKeyDictionary()
self._keepDataAspectRatio = False
@@ -386,6 +381,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
return qt.QSize(8 * 80, 6 * 80) # Mimic MatplotlibBackend
def mousePressEvent(self, event):
+ if event.button() not in self._MOUSE_BTNS:
+ return super(BackendOpenGL, self).mousePressEvent(event)
xPixel = event.x() * self.getDevicePixelRatio()
yPixel = event.y() * self.getDevicePixelRatio()
btn = self._MOUSE_BTNS[event.button()]
@@ -411,6 +408,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
event.accept()
def mouseReleaseEvent(self, event):
+ if event.button() not in self._MOUSE_BTNS:
+ return super(BackendOpenGL, self).mouseReleaseEvent(event)
xPixel = event.x() * self.getDevicePixelRatio()
yPixel = event.y() * self.getDevicePixelRatio()
@@ -462,15 +461,17 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
self._renderOverlayGL()
def _paintFBOGL(self):
- context = glu.getGLContext()
+ context = glu.Context.getCurrent()
plotFBOTex = self._plotFBOs.get(context)
if (self._plot._getDirtyPlot() or self._plotFrame.isDirty or
plotFBOTex is None):
- self._plotVertices = numpy.array(((-1., -1., 0., 0.),
- (1., -1., 1., 0.),
- (-1., 1., 0., 1.),
- (1., 1., 1., 1.)),
- dtype=numpy.float32)
+ self._plotVertices = (
+ # Vertex coordinates
+ numpy.array(((-1., -1.), (1., -1.), (-1., 1.), (1., 1.)),
+ dtype=numpy.float32),
+ # Texture coordinates
+ numpy.array(((0., 0.), (1., 0.), (0., 1.), (1., 1.)),
+ dtype=numpy.float32))
if plotFBOTex is None or \
plotFBOTex.shape[1] != self._plotFrame.size[0] or \
plotFBOTex.shape[0] != self._plotFrame.size[1]:
@@ -502,53 +503,45 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
gl.glUniformMatrix4fv(self._progTex.uniforms['matrix'], 1, gl.GL_TRUE,
mat4Identity().astype(numpy.float32))
- stride = self._plotVertices.shape[-1] * self._plotVertices.itemsize
gl.glEnableVertexAttribArray(self._progTex.attributes['position'])
gl.glVertexAttribPointer(self._progTex.attributes['position'],
2,
gl.GL_FLOAT,
gl.GL_FALSE,
- stride, self._plotVertices)
+ 0,
+ self._plotVertices[0])
- texCoordsPtr = c_void_p(self._plotVertices.ctypes.data +
- 2 * self._plotVertices.itemsize) # Better way?
gl.glEnableVertexAttribArray(self._progTex.attributes['texCoords'])
gl.glVertexAttribPointer(self._progTex.attributes['texCoords'],
2,
gl.GL_FLOAT,
gl.GL_FALSE,
- stride, texCoordsPtr)
+ 0,
+ self._plotVertices[1])
with plotFBOTex.texture:
- gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self._plotVertices))
+ gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self._plotVertices[0]))
self._renderMarkersGL()
self._renderOverlayGL()
def paintGL(self):
- global _current_context
- _current_context = self.context()
-
- glu.setGLContextGetter(_getContext)
-
- # Release OpenGL resources
- for item in self._glGarbageCollector:
- item.discard()
- self._glGarbageCollector = []
-
- gl.glClearColor(*self._backgroundColor)
- gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_STENCIL_BUFFER_BIT)
+ with glu.Context.current(self.context()):
+ # Release OpenGL resources
+ for item in self._glGarbageCollector:
+ item.discard()
+ self._glGarbageCollector = []
- # Check if window is large enough
- plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
- if plotWidth <= 2 or plotHeight <= 2:
- return
+ gl.glClearColor(*self._backgroundColor)
+ gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_STENCIL_BUFFER_BIT)
- # self._paintDirectGL()
- self._paintFBOGL()
+ # Check if window is large enough
+ plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
+ if plotWidth <= 2 or plotHeight <= 2:
+ return
- glu.setGLContextGetter()
- _current_context = None
+ # self._paintDirectGL()
+ self._paintFBOGL()
def _renderMarkersGL(self):
if len(self._markers) == 0:
@@ -892,7 +885,10 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
xErrorMinus, xErrorPlus = xerror[0], xerror[1]
else:
xErrorMinus, xErrorPlus = xerror, xerror
- xErrorMinus = logX - numpy.log10(x - xErrorMinus)
+ with warnings.catch_warnings():
+ warnings.simplefilter('ignore', category=RuntimeWarning)
+ # Ignore divide by zero, invalid value encountered in log10
+ xErrorMinus = logX - numpy.log10(x - xErrorMinus)
xErrorPlus = numpy.log10(x + xErrorPlus) - logX
xerror = numpy.array((xErrorMinus, xErrorPlus),
dtype=numpy.float32)
@@ -912,7 +908,10 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
yErrorMinus, yErrorPlus = yerror[0], yerror[1]
else:
yErrorMinus, yErrorPlus = yerror, yerror
- yErrorMinus = logY - numpy.log10(y - yErrorMinus)
+ with warnings.catch_warnings():
+ warnings.simplefilter('ignore', category=RuntimeWarning)
+ # Ignore divide by zero, invalid value encountered in log10
+ yErrorMinus = logY - numpy.log10(y - yErrorMinus)
yErrorPlus = numpy.log10(y + yErrorPlus) - logY
yerror = numpy.array((yErrorMinus, yErrorPlus),
dtype=numpy.float32)
@@ -1043,6 +1042,25 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
return legend, 'image'
+ def addTriangles(self, x, y, triangles, legend,
+ color, z, selectable, alpha):
+
+ # Handle axes log scale: convert data
+ if self._plotFrame.xAxis.isLog:
+ x = numpy.log10(x)
+ if self._plotFrame.yAxis.isLog:
+ y = numpy.log10(y)
+
+ triangles = GLPlotTriangles(x, y, color, triangles, alpha)
+ triangles.info = {
+ 'legend': legend,
+ 'zOrder': z,
+ 'behaviors': set(['selectable']) if selectable else set(),
+ }
+ self._plotContent.add(triangles)
+
+ return legend, 'triangles'
+
def addItem(self, x, y, legend, shape, color, fill, overlay, z,
linestyle, linewidth, linebgcolor):
# TODO handle overlay
@@ -1132,10 +1150,10 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
self._glGarbageCollector.append(curve)
- elif kind == 'image':
- image = self._plotContent.pop('image', legend)
- if image is not None:
- self._glGarbageCollector.append(image)
+ elif kind in ('image', 'triangles'):
+ item = self._plotContent.pop(kind, legend)
+ if item is not None:
+ self._glGarbageCollector.append(item)
elif kind == 'marker':
self._markers.pop(legend, False)
@@ -1188,6 +1206,60 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
self._plotFrame.size[1] - self._plotFrame.margins.bottom - 1)
return xPlot, yPlot
+ def __pickCurves(self, item, x, y):
+ """Perform picking on a curve item.
+
+ :param GLPlotCurve2D item:
+ :param float x: X position of the mouse in widget coordinates
+ :param float y: Y position of the mouse in widget coordinates
+ :return: List of indices of picked points
+ :rtype: List[int]
+ """
+ offset = self._PICK_OFFSET
+ if item.marker is not None:
+ offset = max(item.markerSize / 2., offset)
+ if item.lineStyle is not None:
+ offset = max(item.lineWidth / 2., offset)
+
+ yAxis = item.info['yAxis']
+
+ inAreaPos = self._mouseInPlotArea(x - offset, y - offset)
+ dataPos = self.pixelToData(inAreaPos[0], inAreaPos[1],
+ axis=yAxis, check=True)
+ if dataPos is None:
+ return []
+ xPick0, yPick0 = dataPos
+
+ inAreaPos = self._mouseInPlotArea(x + offset, y + offset)
+ dataPos = self.pixelToData(inAreaPos[0], inAreaPos[1],
+ axis=yAxis, check=True)
+ if dataPos is None:
+ return []
+ xPick1, yPick1 = dataPos
+
+ if xPick0 < xPick1:
+ xPickMin, xPickMax = xPick0, xPick1
+ else:
+ xPickMin, xPickMax = xPick1, xPick0
+
+ if yPick0 < yPick1:
+ yPickMin, yPickMax = yPick0, yPick1
+ else:
+ yPickMin, yPickMax = yPick1, yPick0
+
+ # Apply log scale if axis is log
+ if self._plotFrame.xAxis.isLog:
+ xPickMin = numpy.log10(xPickMin)
+ xPickMax = numpy.log10(xPickMax)
+
+ if (yAxis == 'left' and self._plotFrame.yAxis.isLog) or (
+ yAxis == 'right' and self._plotFrame.y2Axis.isLog):
+ yPickMin = numpy.log10(yPickMin)
+ yPickMax = numpy.log10(yPickMax)
+
+ return item.pick(xPickMin, yPickMin,
+ xPickMax, yPickMax)
+
def pickItems(self, x, y, kinds):
picked = []
@@ -1236,56 +1308,20 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
picked.append(dict(kind='image',
legend=item.info['legend']))
- elif 'curve' in kinds and isinstance(item, GLPlotCurve2D):
- offset = self._PICK_OFFSET
- if item.marker is not None:
- offset = max(item.markerSize / 2., offset)
- if item.lineStyle is not None:
- offset = max(item.lineWidth / 2., offset)
-
- yAxis = item.info['yAxis']
-
- inAreaPos = self._mouseInPlotArea(x - offset, y - offset)
- dataPos = self.pixelToData(inAreaPos[0], inAreaPos[1],
- axis=yAxis, check=True)
- if dataPos is None:
- continue
- xPick0, yPick0 = dataPos
-
- inAreaPos = self._mouseInPlotArea(x + offset, y + offset)
- dataPos = self.pixelToData(inAreaPos[0], inAreaPos[1],
- axis=yAxis, check=True)
- if dataPos is None:
- continue
- xPick1, yPick1 = dataPos
-
- if xPick0 < xPick1:
- xPickMin, xPickMax = xPick0, xPick1
- else:
- xPickMin, xPickMax = xPick1, xPick0
-
- if yPick0 < yPick1:
- yPickMin, yPickMax = yPick0, yPick1
- else:
- yPickMin, yPickMax = yPick1, yPick0
-
- # Apply log scale if axis is log
- if self._plotFrame.xAxis.isLog:
- xPickMin = numpy.log10(xPickMin)
- xPickMax = numpy.log10(xPickMax)
-
- if (yAxis == 'left' and self._plotFrame.yAxis.isLog) or (
- yAxis == 'right' and self._plotFrame.y2Axis.isLog):
- yPickMin = numpy.log10(yPickMin)
- yPickMax = numpy.log10(yPickMax)
-
- pickedIndices = item.pick(xPickMin, yPickMin,
- xPickMax, yPickMax)
- if pickedIndices:
- picked.append(dict(kind='curve',
- legend=item.info['legend'],
- indices=pickedIndices))
-
+ elif 'curve' in kinds:
+ if isinstance(item, GLPlotCurve2D):
+ pickedIndices = self.__pickCurves(item, x, y)
+ if pickedIndices:
+ picked.append(dict(kind='curve',
+ legend=item.info['legend'],
+ indices=pickedIndices))
+
+ elif isinstance(item, GLPlotTriangles):
+ pickedIndices = item.pick(*dataPos)
+ if pickedIndices:
+ picked.append(dict(kind='curve',
+ legend=item.info['legend'],
+ indices=pickedIndices))
return picked
# Update curve
diff --git a/silx/gui/plot/backends/glutils/GLPlotTriangles.py b/silx/gui/plot/backends/glutils/GLPlotTriangles.py
new file mode 100644
index 0000000..c756749
--- /dev/null
+++ b/silx/gui/plot/backends/glutils/GLPlotTriangles.py
@@ -0,0 +1,193 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""
+This module provides a class to render a set of 2D triangles
+"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "03/04/2017"
+
+
+import ctypes
+
+import numpy
+
+from .....math.combo import min_max
+from .... import _glutils as glutils
+from ...._glutils import gl
+
+
+class GLPlotTriangles(object):
+ """Handle rendering of a set of colored triangles"""
+
+ _PROGRAM = glutils.Program(
+ vertexShader="""
+ #version 120
+
+ uniform mat4 matrix;
+ attribute float xPos;
+ attribute float yPos;
+ attribute vec4 color;
+
+ varying vec4 vColor;
+
+ void main(void) {
+ gl_Position = matrix * vec4(xPos, yPos, 0.0, 1.0);
+ vColor = color;
+ }
+ """,
+ fragmentShader="""
+ #version 120
+
+ uniform float alpha;
+ varying vec4 vColor;
+
+ void main(void) {
+ gl_FragColor = vColor;
+ gl_FragColor.a *= alpha;
+ }
+ """,
+ attrib0='xPos')
+
+ def __init__(self, x, y, color, triangles, alpha=1.):
+ """
+
+ :param numpy.ndarray x: X coordinates of triangle corners
+ :param numpy.ndarray y: Y coordinates of triangle corners
+ :param numpy.ndarray color: color for each point
+ :param numpy.ndarray triangles: (N, 3) array of indices of triangles
+ :param float alpha: Opacity in [0, 1]
+ """
+ # Check and convert input data
+ x = numpy.ravel(numpy.array(x, dtype=numpy.float32))
+ y = numpy.ravel(numpy.array(y, dtype=numpy.float32))
+ color = numpy.array(color, copy=False)
+ # Cast to uint32
+ triangles = numpy.array(triangles, copy=False, dtype=numpy.uint32)
+
+ assert x.size == y.size
+ assert x.size == len(color)
+ assert color.ndim == 2 and color.shape[1] in (3, 4)
+ if numpy.issubdtype(color.dtype, numpy.floating):
+ color = numpy.array(color, dtype=numpy.float32, copy=False)
+ elif numpy.issubdtype(color.dtype, numpy.integer):
+ color = numpy.array(color, dtype=numpy.uint8, copy=False)
+ else:
+ raise ValueError('Unsupported color type')
+ assert triangles.ndim == 2 and triangles.shape[1] == 3
+
+ self.__x_y_color = x, y, color
+ self.xMin, self.xMax = min_max(x, finite=True)
+ self.yMin, self.yMax = min_max(y, finite=True)
+ self.__triangles = triangles
+ self.__alpha = numpy.clip(float(alpha), 0., 1.)
+ self.__vbos = None
+ self.__indicesVbo = None
+ self.__picking_triangles = None
+
+ def pick(self, x, y):
+ """Perform picking
+
+ :param float x: X coordinates in plot data frame
+ :param float y: Y coordinates in plot data frame
+ :return: List of picked data point indices
+ :rtype: numpy.ndarray
+ """
+ if (x < self.xMin or x > self.xMax or
+ y < self.yMin or y > self.yMax):
+ return ()
+ xPts, yPts = self.__x_y_color[:2]
+ if self.__picking_triangles is None:
+ self.__picking_triangles = numpy.zeros(
+ self.__triangles.shape + (3,), dtype=numpy.float32)
+ self.__picking_triangles[:, :, 0] = xPts[self.__triangles]
+ self.__picking_triangles[:, :, 1] = yPts[self.__triangles]
+
+ segment = numpy.array(((x, y, -1), (x, y, 1)), dtype=numpy.float32)
+ # Picked triangle indices
+ indices = glutils.segmentTrianglesIntersection(
+ segment, self.__picking_triangles)[0]
+ # Point indices
+ indices = numpy.unique(numpy.ravel(self.__triangles[indices]))
+
+ # Sorted from furthest to closest point
+ dists = (xPts[indices] - x) ** 2 + (yPts[indices] - y) ** 2
+ indices = indices[numpy.flip(numpy.argsort(dists))]
+
+ return tuple(indices)
+
+ def discard(self):
+ """Release resources on the GPU"""
+ if self.__vbos is not None:
+ self.__vbos[0].vbo.discard()
+ self.__vbos = None
+ self.__indicesVbo.discard()
+ self.__indicesVbo = None
+
+ def prepare(self):
+ """Allocate resources on the GPU"""
+ if self.__vbos is None:
+ self.__vbos = glutils.vertexBuffer(self.__x_y_color)
+ # Normalization is need for color
+ self.__vbos[-1].normalization = True
+
+ if self.__indicesVbo is None:
+ self.__indicesVbo = glutils.VertexBuffer(
+ numpy.ravel(self.__triangles),
+ usage=gl.GL_STATIC_DRAW,
+ target=gl.GL_ELEMENT_ARRAY_BUFFER)
+
+ def render(self, matrix, isXLog, isYLog):
+ """Perform rendering
+
+ :param numpy.ndarray matrix: 4x4 transform matrix to use
+ :param bool isXLog:
+ :param bool isYLog:
+ """
+ self.prepare()
+
+ if self.__vbos is None or self.__indicesVbo is None:
+ return # Nothing to display
+
+ self._PROGRAM.use()
+
+ gl.glUniformMatrix4fv(self._PROGRAM.uniforms['matrix'],
+ 1,
+ gl.GL_TRUE,
+ matrix.astype(numpy.float32))
+
+ gl.glUniform1f(self._PROGRAM.uniforms['alpha'], self.__alpha)
+
+ for index, name in enumerate(('xPos', 'yPos', 'color')):
+ attr = self._PROGRAM.attributes[name]
+ gl.glEnableVertexAttribArray(attr)
+ self.__vbos[index].setVertexAttrib(attr)
+
+ with self.__indicesVbo:
+ gl.glDrawElements(gl.GL_TRIANGLES,
+ self.__triangles.size,
+ glutils.numpyToGLType(self.__triangles.dtype),
+ ctypes.c_void_p(0))
diff --git a/silx/gui/plot/backends/glutils/GLText.py b/silx/gui/plot/backends/glutils/GLText.py
index 3d262bc..725c12c 100644
--- a/silx/gui/plot/backends/glutils/GLText.py
+++ b/silx/gui/plot/backends/glutils/GLText.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -33,9 +33,11 @@ __date__ = "03/04/2017"
from collections import OrderedDict
+import weakref
+
import numpy
-from ...._glutils import font, gl, getGLContext, Program, Texture
+from ...._glutils import font, gl, Context, Program, Texture
from .GLSupport import mat4Translate
@@ -128,7 +130,7 @@ class Text2D(object):
attrib0='position')
# Discard texture objects when removed from the cache
- _textures = _Cache(callback=lambda key, value: value[0].discard())
+ _textures = weakref.WeakKeyDictionary()
"""Cache already created textures"""
_sizes = _Cache()
@@ -159,15 +161,20 @@ class Text2D(object):
self._rotate = numpy.radians(rotate)
def _getTexture(self, text):
- key = getGLContext(), text
-
- if key not in self._textures:
+ # Retrieve/initialize texture cache for current context
+ context = Context.getCurrent()
+ if context not in self._textures:
+ self._textures[context] = _Cache(
+ callback=lambda key, value: value[0].discard())
+ textures = self._textures[context]
+
+ if text not in textures:
image, offset = font.rasterText(text,
font.getDefaultFontFamily())
if text not in self._sizes:
self._sizes[text] = image.shape[1], image.shape[0]
- self._textures[key] = (
+ textures[text] = (
Texture(gl.GL_RED,
data=image,
minFilter=gl.GL_NEAREST,
@@ -176,7 +183,7 @@ class Text2D(object):
gl.GL_CLAMP_TO_EDGE)),
offset)
- return self._textures[key]
+ return textures[text]
@property
def text(self):
diff --git a/silx/gui/plot/backends/glutils/GLTexture.py b/silx/gui/plot/backends/glutils/GLTexture.py
index 25dd9f1..118a36f 100644
--- a/silx/gui/plot/backends/glutils/GLTexture.py
+++ b/silx/gui/plot/backends/glutils/GLTexture.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -163,7 +163,6 @@ class Image(object):
data[yOrig:yOrig+hData,
xOrig:xOrig+wData],
format_,
- shape=(hData, wData),
texUnit=texUnit,
minFilter=self._MIN_FILTER,
magFilter=self._MAG_FILTER,
diff --git a/silx/gui/plot/backends/glutils/__init__.py b/silx/gui/plot/backends/glutils/__init__.py
index 771de39..d58c084 100644
--- a/silx/gui/plot/backends/glutils/__init__.py
+++ b/silx/gui/plot/backends/glutils/__init__.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -39,6 +39,7 @@ _logger = logging.getLogger(__name__)
from .GLPlotCurve import * # noqa
from .GLPlotFrame import * # noqa
from .GLPlotImage import * # noqa
+from .GLPlotTriangles import GLPlotTriangles # noqa
from .GLSupport import * # noqa
from .GLText import * # noqa
from .GLTexture import * # noqa