Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

silx.gui.plot.PlotWidget: Improved line dash rendering for OpenGL backend #4015

Merged
merged 3 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 31 additions & 12 deletions src/silx/gui/plot/backends/BackendOpenGL.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__()

Expand All @@ -84,8 +84,8 @@ def __init__(
"fill": "hatch" if fill else None,
"x": x,
"y": y,
"linestyle": linestyle,
"linewidth": linewidth,
"dashpattern": dashpattern,
"gapcolor": gapcolor,
}
)
Expand All @@ -99,8 +99,8 @@ def __init__(
text,
color,
symbol,
linestyle,
linewidth,
dashpattern,
constraint,
yaxis,
font,
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -859,6 +859,22 @@ def _castArrayTo(v):
else:
raise ValueError("Unsupported data type")

_DASH_PATTERNS = { # Convert from linestyle to dash pattern
"": None,
" ": None,
"-": (),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty tuple = solid line (no dashes). Not really nice but simple.

"--": (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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
85 changes: 23 additions & 62 deletions src/silx/gui/plot/backends/glutils/GLPlotCurve.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,6 @@ def isInitialized(self):

# line ########################################################################

SOLID, DASHED, DASHDOT, DOTTED = "-", "--", "-.", ":"


class GLLines2D(object):
"""Object rendering curve as a polyline
Expand All @@ -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
Expand Down Expand Up @@ -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),
):
Expand Down Expand Up @@ -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"""
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand All @@ -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"))

Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 = (
Expand Down