diff --git a/src/silx/gui/plot/backends/BackendOpenGL.py b/src/silx/gui/plot/backends/BackendOpenGL.py index 035cc767fd..10eaca7bd4 100755 --- a/src/silx/gui/plot/backends/BackendOpenGL.py +++ b/src/silx/gui/plot/backends/BackendOpenGL.py @@ -58,7 +58,7 @@ class _ShapeItem(dict): def __init__( - self, x, y, shape, color, fill, overlay, linestyle, linewidth, gapcolor + self, x, y, shape, color, fill, overlay, linewidth, dashpattern, gapcolor ): super(_ShapeItem, self).__init__() @@ -84,8 +84,8 @@ def __init__( "fill": "hatch" if fill else None, "x": x, "y": y, - "linestyle": linestyle, "linewidth": linewidth, + "dashpattern": dashpattern, "gapcolor": gapcolor, } ) @@ -99,8 +99,8 @@ def __init__( text, color, symbol, - linestyle, linewidth, + dashpattern, constraint, yaxis, font, @@ -124,8 +124,8 @@ def __init__( "color": colors.rgba(color), "constraint": constraint if isConstraint else None, "symbol": symbol, - "linestyle": linestyle, "linewidth": linewidth, + "dashpattern": dashpattern, "yaxis": yaxis, "font": font, "bgcolor": bgcolor, @@ -575,7 +575,7 @@ def _renderItems(self, overlay=False): ) # Draw the stroke - if item["linestyle"] not in ("", " ", None): + if item["dashpattern"] is not None: if item["shape"] != "polylines": # close the polyline points = numpy.append( @@ -585,10 +585,10 @@ def _renderItems(self, overlay=False): lines = glutils.GLLines2D( points[:, 0], points[:, 1], - style=item["linestyle"], color=item["color"], gapColor=item["gapcolor"], width=item["linewidth"], + dashPattern=item["dashpattern"], ) context.matrix = self.matScreenProj lines.render(context) @@ -636,9 +636,9 @@ def _renderItems(self, overlay=False): lines = glutils.GLLines2D( (0, width), (pixelPos[1], pixelPos[1]), - style=item["linestyle"], color=color, width=item["linewidth"], + dashPattern=item["dashpattern"], ) context.matrix = self.matScreenProj lines.render(context) @@ -669,9 +669,9 @@ def _renderItems(self, overlay=False): lines = glutils.GLLines2D( (pixelPos[0], pixelPos[0]), (0, height), - style=item["linestyle"], color=color, width=item["linewidth"], + dashPattern=item["dashpattern"], ) context.matrix = self.matScreenProj lines.render(context) @@ -859,6 +859,22 @@ def _castArrayTo(v): else: raise ValueError("Unsupported data type") + _DASH_PATTERNS = { # Convert from linestyle to dash pattern + "": None, + " ": None, + "-": (), + "--": (3.7, 1.6, 3.7, 1.6), + "-.": (6.4, 1.6, 1, 1.6), + ":": (1, 1.65, 1, 1.65), + None: None, + } + + def _lineStyleToDashPattern( + self, style: str | None + ) -> tuple[float, float, float, float] | tuple[()] | None: + """Convert a linestyle to its corresponding dash pattern""" + return self._DASH_PATTERNS[style] + def addCurve( self, x, @@ -977,16 +993,17 @@ def addCurve( fillColor = None if fill is True: fillColor = color + curve = glutils.GLPlotCurve2D( x, y, colorArray, xError=xerror, yError=yerror, - lineStyle=linestyle, lineColor=color, lineGapColor=gapcolor, lineWidth=linewidth, + lineDashPattern=self._lineStyleToDashPattern(linestyle), marker=symbol, markerColor=color, markerSize=symbolsize, @@ -1091,8 +1108,9 @@ def addShape( if self._plotFrame.yAxis.isLog and y.min() <= 0.0: raise RuntimeError("Cannot add item with Y <= 0 with Y axis log scale") + dashpattern = self._lineStyleToDashPattern(linestyle) return _ShapeItem( - x, y, shape, color, fill, overlay, linestyle, linewidth, gapcolor + x, y, shape, color, fill, overlay, linewidth, dashpattern, gapcolor ) def addMarker( @@ -1110,14 +1128,15 @@ def addMarker( bgcolor: RGBAColorType | None, ): font = qt.QApplication.instance().font() if font is None else font + dashpattern = self._lineStyleToDashPattern(linestyle) return _MarkerItem( x, y, text, color, symbol, - linestyle, linewidth, + dashpattern, constraint, yaxis, font, @@ -1209,7 +1228,7 @@ def __pickCurves(self, item, x, y): qtDpi = self.getDotsPerInch() / self.getDevicePixelRatio() size = item.markerSize / 72.0 * qtDpi offset = max(size / 2.0, offset) - if item.lineStyle is not None: + if item.lineDashPattern is not None: # Convert line width from points to qt pixels qtDpi = self.getDotsPerInch() / self.getDevicePixelRatio() lineWidth = item.lineWidth / 72.0 * qtDpi diff --git a/src/silx/gui/plot/backends/glutils/GLPlotCurve.py b/src/silx/gui/plot/backends/glutils/GLPlotCurve.py index 71494141d1..54acdc6f7c 100644 --- a/src/silx/gui/plot/backends/glutils/GLPlotCurve.py +++ b/src/silx/gui/plot/backends/glutils/GLPlotCurve.py @@ -278,8 +278,6 @@ def isInitialized(self): # line ######################################################################## -SOLID, DASHED, DASHDOT, DOTTED = "-", "--", "-.", ":" - class GLLines2D(object): """Object rendering curve as a polyline @@ -288,17 +286,16 @@ class GLLines2D(object): :param yVboData: Y coordinates VBO :param colorVboData: VBO of colors :param distVboData: VBO of distance along the polyline - :param str style: Line style in: '-', '--', '-.', ':' :param List[float] color: RGBA color as 4 float in [0, 1] :param float width: Line width - :param float dashPeriod: Period of dashes + :param List[float] dashPattern: + "unscaled" dash pattern as 4 lengths in points (dash1, gap1, dash2, gap2). + This pattern is scaled with the line width. + Set to () to draw solid lines (default), and to None to disable rendering. :param drawMode: OpenGL drawing mode :param List[float] offset: Translation of coordinates (ox, oy) """ - STYLES = SOLID, DASHED, DASHDOT, DOTTED - """Supported line styles""" - _SOLID_PROGRAM = Program( vertexShader=""" #version 120 @@ -383,11 +380,10 @@ def __init__( yVboData=None, colorVboData=None, distVboData=None, - style=SOLID, color=(0.0, 0.0, 0.0, 1.0), gapColor=None, width=1, - dashPeriod=10.0, + dashPattern=(), drawMode=None, offset=(0.0, 0.0), ): @@ -419,26 +415,11 @@ def __init__( self.color = color self.gapColor = gapColor self.width = width - self._style = None - self.style = style - self.dashPeriod = dashPeriod + self.dashPattern = dashPattern self.offset = offset self._drawMode = drawMode if drawMode is not None else gl.GL_LINE_STRIP - @property - def style(self): - """Line style (Union[str,None])""" - return self._style - - @style.setter - def style(self, style): - if style in _MPL_NONES: - self._style = None - else: - assert style in self.STYLES - self._style = style - @classmethod def init(cls): """OpenGL context initialization""" @@ -449,39 +430,23 @@ def render(self, context): :param RenderContext context: """ - width = self.width / 72.0 * context.dpi - - style = self.style - if style is None: + if self.dashPattern is None: # Nothing to display return - elif style == SOLID: + if self.dashPattern == (): # No dash: solid line program = self._SOLID_PROGRAM program.use() - else: # DASHED, DASHDOT, DOTTED + else: # Dashed line defined by 4 control points program = self._DASH_PROGRAM program.use() - dashPeriod = self.dashPeriod * width - if self.style == DOTTED: - dash = ( - 0.2 * dashPeriod, - 0.5 * dashPeriod, - 0.7 * dashPeriod, - dashPeriod, - ) - elif self.style == DASHDOT: - dash = ( - 0.3 * dashPeriod, - 0.5 * dashPeriod, - 0.6 * dashPeriod, - dashPeriod, - ) - else: - dash = (0.5 * dashPeriod, dashPeriod, dashPeriod, dashPeriod) - - gl.glUniform4f(program.uniforms["dash"], *dash) + # Scale pattern by width, convert from lengths in points to offsets in pixels + scale = self.width / 72.0 * context.dpi + dashOffsets = tuple( + offset * scale for offset in numpy.cumsum(self.dashPattern) + ) + gl.glUniform4f(program.uniforms["dash"], *dashOffsets) if self.gapColor is None: # Use fully transparent color which gets discarded in shader @@ -540,7 +505,7 @@ def render(self, context): yPosAttrib, 1, gl.GL_FLOAT, False, 0, self.yVboData ) - gl.glLineWidth(width) + gl.glLineWidth(self.width / 72.0 * context.dpi) gl.glDrawArrays(self._drawMode, 0, self.xVboData.size) gl.glDisable(gl.GL_LINE_SMOOTH) @@ -1220,11 +1185,10 @@ def __init__( colorData=None, xError=None, yError=None, - lineStyle=SOLID, lineColor=(0.0, 0.0, 0.0, 1.0), lineGapColor=None, lineWidth=1, - lineDashPeriod=20, + lineDashPattern=(), marker=SQUARE, markerColor=(0.0, 0.0, 0.0, 1.0), markerSize=7, @@ -1311,11 +1275,10 @@ def deduce_baseline(baseline): ) self.lines = GLLines2D() - self.lines.style = lineStyle self.lines.color = lineColor self.lines.gapColor = lineGapColor self.lines.width = lineWidth - self.lines.dashPeriod = lineDashPeriod + self.lines.dashPattern = lineDashPattern self.lines.offset = self.offset self.points = Points2D() @@ -1336,15 +1299,13 @@ def deduce_baseline(baseline): distVboData = _proxyProperty(("lines", "distVboData")) - lineStyle = _proxyProperty(("lines", "style")) - lineColor = _proxyProperty(("lines", "color")) lineGapColor = _proxyProperty(("lines", "gapColor")) lineWidth = _proxyProperty(("lines", "width")) - lineDashPeriod = _proxyProperty(("lines", "dashPeriod")) + lineDashPattern = _proxyProperty(("lines", "dashPattern")) marker = _proxyProperty(("points", "marker")) @@ -1362,7 +1323,7 @@ def prepare(self): """Rendering preparation: build indices and bounding box vertices""" if self.xVboData is None: xAttrib, yAttrib, cAttrib, dAttrib = None, None, None, None - if self.lineStyle in (DASHED, DASHDOT, DOTTED): + if self.lineDashPattern: dists = distancesFromArrays(self.xData, self.yData, self._ratio) if self.colorData is None: xAttrib, yAttrib, dAttrib = vertexBuffer( @@ -1393,7 +1354,7 @@ def render(self, context): :param RenderContext context: Rendering information """ - if self.lineStyle in (DASHED, DASHDOT, DOTTED): + if self.lineDashPattern: visibleRanges = context.plotFrame.transformedDataRanges xLimits = visibleRanges.x yLimits = visibleRanges.y if self.yaxis == "left" else visibleRanges.y2 @@ -1450,7 +1411,7 @@ def pick(self, xPickMin, yPickMin, xPickMax, yPickMax): :rtype: Union[List[int],None] """ if ( - (self.marker is None and self.lineStyle is None) + (self.marker is None and self.lineDashPattern is None) or self.xMin > xPickMax or xPickMin > self.xMax or self.yMin > yPickMax @@ -1464,7 +1425,7 @@ def pick(self, xPickMin, yPickMin, xPickMax, yPickMax): yPickMin = yPickMin - self.offset[1] yPickMax = yPickMax - self.offset[1] - if self.lineStyle is not None: + if self.lineDashPattern is not None: # Using Cohen-Sutherland algorithm for line clipping with numpy.errstate(invalid="ignore"): # Ignore NaN comparison warnings codes = (