#
# lightboxcanvas.py - The LightBoxCanvas class.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`LightBoxCanvas` class, which is a
:class:`.SliceCanvas` that displays multiple 2D slices along a single axis
from a collection of 3D overlays.
"""
import sys
import logging
import numpy as np
import OpenGL.GL as gl
import fsl.data.image as fslimage
import fsl.transform.affine as affine
import fsleyes.displaycontext.canvasopts as canvasopts
import fsleyes.gl.slicecanvas as slicecanvas
import fsleyes.gl.resources as glresources
import fsleyes.gl.routines as glroutines
import fsleyes.gl.textures as textures
log = logging.getLogger(__name__)
[docs]class LightBoxCanvas(slicecanvas.SliceCanvas):
"""The ``LightBoxCanvas`` represents an OpenGL canvas which displays
multiple slices from a collection of 3D overlays. The slices are laid
out on the same canvas along rows and columns, with the slice at the
minimum Z position translated to the top left of the canvas, and the
slice with the maximum Z value translated to the bottom right.
.. note:: The :class:`LightBoxCanvas` class is not intended to be
instantiated directly - use one of these subclasses, depending
on your use-case:
- :class:`.OSMesaLightBoxCanvas` for static off-screen
rendering of a scene using OSMesa.
- :class:`.WXGLLightBoxCanvas` for interactive rendering on a
:class:`wx.glcanvas.GLCanvas` canvas.
The ``LightBoxCanvas`` class derives from the :class:`.SliceCanvas` class,
and is tightly coupled to the ``SliceCanvas`` implementation. Various
settings, and the current scene displayed on a ``LightBoxCanvas``
instance, can be changed through the properties of the
``LightBoxCanvasOpts`` instance, available via the :attr:`opts` attribute.
Performance of a ``LightBoxCanvas`` instance may be controlled through the
:attr:`.SliceCanvasOpts.renderMode` property, in the same way as for the
:class:`.SliceCanvas`. However, the ``LightBoxCanvas`` handles the
``offscreen`` render mode differently to the ``SliceCanvas. Where the
``SliceCanvas`` uses a separate :class:`.RenderTexture` for every overlay
in the :class:`.OverlayList`, the ``LightBoxCanvas`` uses a single
``RenderTexture`` to render all overlays off-screen.
The ``LightBoxCanvas`` class defines the following convenience methods (in
addition to those defined in the ``SliceCanvas`` class):
.. autosummary::
:nosignatures:
canvasToWorld
worldToCanvas
getTotalRows
calcSliceSpacing
"""
[docs] def __init__(self, overlayList, displayCtx, zax=0):
"""Create a ``LightBoxCanvas`` object.
:arg overlayList: An :class:`.OverlayList` object which contains a
list of overlays to be displayed.
:arg displayCtx: A :class:`.DisplayContext` object which defines how
that overlay list is to be displayed.
:arg zax: Display coordinate system axis to be used as the
'depth' axis. Can be changed via the
:attr:`.SliceCanvas.zax` property.
"""
# These attributes are used to keep track of
# the total number of slices to be displayed,
# and the total number of rows to be displayed
self._nslices = 0
self._totalRows = 0
# The final display bounds calculated by
# SliceCanvas._updateDisplayBounds is not
# necessarily the same as the actual bounds,
# as they are adjusted to preserve the
# image aspect ratio. But the real bounds
# are of use in the _zPosChanged method, so
# we save them here as an attribute -
# see _updateDisplayBounds.
self._realBounds = None
# This will point to a RenderTexture if
# the offscreen render mode is enabled
self._offscreenRenderTexture = None
opts = canvasopts.LightBoxCanvasOpts()
slicecanvas.SliceCanvas.__init__(self,
overlayList,
displayCtx,
zax,
opts)
opts.addListener('sliceSpacing', self.name, self._slicePropsChanged)
opts.addListener('ncols', self.name, self._slicePropsChanged)
opts.addListener('nrows', self.name, self._slicePropsChanged)
opts.addListener('zrange', self.name, self._slicePropsChanged)
opts.addListener('showGridLines', self.name, self.Refresh)
opts.addListener('highlightSlice', self.name, self.Refresh)
opts.addListener('topRow', self.name, self._topRowChanged)
# Add a listener to the position so when it
# changes we can adjust the display range (via
# the topRow property) to ensure the slice
# corresponding to the current z position is
# visible. SliceCanvas.__init__ has already
# registered a listener, on pos, with
# self.name - so we use a different name
# here
opts.addListener('pos',
'{}_zPosChanged'.format(self.name),
self._zPosChanged)
[docs] def destroy(self):
"""Overrides :meth:`.SliceCanvas.destroy`. Must be called when this
``LightBoxCanvas`` is no longer needed.
Removes some property listeners, makes sure that the off screen
:class:`.RenderTexture` (if one exists) is destroyed, and then calls
the :meth:`.SliceCanvas.destroy` method.
"""
opts = self.opts
opts.removeListener('pos', '{}_zPosChanged'.format(self.name))
opts.removeListener('sliceSpacing', self.name)
opts.removeListener('ncols', self.name)
opts.removeListener('nrows', self.name)
opts.removeListener('zrange', self.name)
opts.removeListener('showGridLines', self.name)
opts.removeListener('highlightSlice', self.name)
opts.removeListener('topRow', self.name)
if self._offscreenRenderTexture is not None:
self._offscreenRenderTexture.destroy()
slicecanvas.SliceCanvas.destroy(self)
[docs] def worldToCanvas(self, pos):
"""Given an x/y/z location in the display coordinate system, converts
it into an x/y position, in the coordinate system of this
``LightBoxCanvas``.
"""
opts = self.opts
xpos = pos[opts.xax]
ypos = pos[opts.yax]
zpos = pos[opts.zax]
sliceno = int(np.floor((zpos - opts.zrange.xlo) / opts.sliceSpacing))
xlen = self.displayCtx.bounds.getLen(opts.xax)
ylen = self.displayCtx.bounds.getLen(opts.yax)
xmin = self.displayCtx.bounds.getLo( opts.xax)
ymin = self.displayCtx.bounds.getLo( opts.yax)
row = self._totalRows - int(np.floor(sliceno / opts.ncols)) - 1
col = int(np.floor(sliceno % opts.ncols))
if opts.invertX: xpos = xmin + xlen - (xpos - xmin)
if opts.invertY: ypos = ymin + ylen - (ypos - ymin)
xpos = xpos + xlen * col
ypos = ypos + ylen * row
return xpos, ypos
[docs] def canvasToWorld(self, xpos, ypos):
"""Overrides :meth:.SliceCanvas.canvasToWorld`.
Given pixel x/y coordinates on this canvas, translates them into the
corresponding display space x/y/z coordinates. Returns a 3-tuple
containing the (x, y, z) display system coordinates. If the given
canvas position is out of the :attr:`.SliceCanvas.displayBounds`,
``None`` is returned.
"""
opts = self.opts
nrows = self._totalRows
ncols = opts.ncols
screenPos = slicecanvas.SliceCanvas.canvasToWorld(
self, xpos, ypos, invertX=False, invertY=False)
if screenPos is None:
return None
screenx = screenPos[opts.xax]
screeny = screenPos[opts.yax]
xmin = self.displayCtx.bounds.getLo( opts.xax)
ymin = self.displayCtx.bounds.getLo( opts.yax)
xlen = self.displayCtx.bounds.getLen(opts.xax)
ylen = self.displayCtx.bounds.getLen(opts.yax)
xmax = xmin + ncols * xlen
ymax = ymin + nrows * ylen
col = int(np.floor((screenx - xmin) / xlen))
row = nrows - int(np.floor((screeny - ymin) / ylen)) - 1
sliceno = row * ncols + col
if screenx < xmin or \
screenx > xmax or \
screeny < ymin or \
screeny > ymax or \
sliceno < 0 or \
sliceno >= self._nslices:
return None
xpos = screenx - col * xlen
ypos = screeny - (nrows - row - 1) * ylen
if opts.invertX: xpos = xlen - (xpos - xmin) + xmin
if opts.invertY: ypos = ylen - (ypos - ymin) + ymin
zpos = opts.zrange.xlo + (sliceno + 0.5) * opts.sliceSpacing
pos = [0, 0, 0]
pos[opts.xax] = xpos
pos[opts.yax] = ypos
pos[opts.zax] = zpos
return tuple(pos)
[docs] def getTotalRows(self):
"""Returns the total number of rows that may be displayed. """
return self._totalRows
[docs] def calcSliceSpacing(self, overlay):
"""Calculates and returns a Z-axis slice spacing value suitable
for the given overlay.
"""
copts = self.opts
displayCtx = self.displayCtx
dopts = displayCtx.getOpts(overlay)
zmin, zmax = dopts.bounds.getRange(copts.zax)
overlay = displayCtx.getReferenceImage(overlay)
# If the overlay does not have a
# reference NIFTI image, choose
# an arbitrary slice spacing.
if overlay is None:
return (zmax - zmin) / 50.0
# Get the DisplayOpts instance
# for the reference image
dopts = displayCtx.getOpts(overlay)
# Otherwise return a spacing
# appropriate for the current
# display space
if dopts.transform == 'id': return 1
elif dopts.transform == 'pixdim': return overlay.pixdim[copts.zax]
elif dopts.transform == 'pixdim-flip': return overlay.pixdim[copts.zax]
elif dopts.transform == 'affine': return min(overlay.pixdim[:3])
# This overlay is being displayed with a
# custrom transformation matrix - check
# to see what display space we're in
displaySpace = displayCtx.displaySpace
if isinstance(displaySpace, fslimage.Nifti) and \
overlay is not displaySpace:
return self.calcSliceSpacing(displaySpace)
else:
return min(overlay.pixdim[:3])
[docs] def _zAxisChanged(self, *a):
"""Overrides :meth:`.SliceCanvas._zAxisChanged`. Calls that
method, and then resets the :attr:`sliceSpacing` and :attr:`zrange`
properties to sensible values.
"""
slicecanvas.SliceCanvas._zAxisChanged(self, *a)
self._updateZAxisProperties()
self._slicePropsChanged()
[docs] def _topRowChanged(self, *a):
"""Called when the :attr:`topRow` property changes. Adjusts display
range and refreshes the canvas.
"""
self._updateDisplayBounds()
self.Refresh()
[docs] def _slicePropsChanged(self, *a):
"""Called when any of the slice properties change. Regenerates slice
locations and display bounds, and refreshes the canvas.
"""
self._calcNumSlices()
self._genSliceLocations()
self._zPosChanged()
self._updateDisplayBounds()
if log.getEffectiveLevel() == logging.DEBUG:
opts = self.opts
props = [('nrows', opts.nrows),
('ncols', opts.ncols),
('sliceSpacing', opts.sliceSpacing),
('zrange', opts.zrange)]
props = '; '.join(['{}={}'.format(k, v) for k, v in props])
log.debug('Lightbox properties changed: [{}]'.format(props))
self.Refresh()
[docs] def _renderModeChange(self, *a):
"""Overrides :meth:`.SliceCanvas._renderModeChange`. Makes sure that
any off-screen :class:`.RenderTexture` is destroyed, and calls the
:meth:`.SliceCanvas._renderModeChange` method.
"""
if self._offscreenRenderTexture is not None:
self._offscreenRenderTexture.destroy()
self._offscreenRenderTexture = None
slicecanvas.SliceCanvas._renderModeChange(self, *a)
[docs] def _updateRenderTextures(self):
"""Overrides :meth:`.SliceCanvas._updateRenderTextures`.
Destroys/creates :class:`.RenderTexture` and
:class:`.RenderTextureStack` instances as needed.
"""
renderMode = self.opts.renderMode
if renderMode == 'onscreen':
return
# The LightBoxCanvas does offscreen rendering
# a bit different to the SliceCanvas. The latter
# uses a separate render texture for each overlay
# whereas here we're going to use a single
# render texture for all overlays.
elif renderMode == 'offscreen':
if self._offscreenRenderTexture is not None:
self._offscreenRenderTexture.destroy()
self._offscreenRenderTexture = textures.RenderTexture(
'{}_{}'.format(type(self).__name__, id(self)),
interp=gl.GL_LINEAR)
self._offscreenRenderTexture.shape = 768, 768
# The LightBoxCanvas handles pre-render mode
# the same way as the SliceCanvas - a separate
# RenderTextureStack for eacn globject
elif renderMode == 'prerender':
# Delete any RenderTextureStack instances for
# overlays which have been removed from the list
for overlay, (tex, name) in list(self._prerenderTextures.items()):
if overlay not in self.overlayList:
self._prerenderTextures.pop(overlay)
glresources.delete(name)
# Create a RendeTextureStack for overlays
# which have been added to the list
for overlay in self.overlayList:
if overlay in self._prerenderTextures:
continue
globj = self._glObjects.get(overlay, None)
if (globj is None) or (not globj):
continue
rt, name = self._getPreRenderTexture(globj, overlay)
self._prerenderTextures[overlay] = rt, name
self.Refresh()
[docs] def _calcNumSlices(self, *a):
"""Calculates the total number of slices to be displayed and
the total number of rows.
"""
opts = self.opts
xlen = self.displayCtx.bounds.getLen(opts.xax)
ylen = self.displayCtx.bounds.getLen(opts.yax)
zlen = opts.zrange.xlen
width, height = self.GetSize()
if xlen == 0 or \
ylen == 0 or \
width == 0 or \
height == 0:
return
self._nslices = int(np.ceil(zlen / opts.sliceSpacing))
self._totalRows = int(np.ceil(self._nslices / float(opts.ncols)))
if self._nslices == 0 or self._totalRows == 0:
return
# All slices are going to be displayed, so
# we'll 'disable' the topRow property
if self._totalRows < opts.nrows:
opts.setAttribute('topRow', 'minval', 0)
opts.setAttribute('topRow', 'maxval', 0)
# nrows slices are going to be displayed,
# and the topRow property can be used to
# scroll through all available rows.
else:
opts.setAttribute('topRow', 'maxval', self._totalRows - opts.nrows)
[docs] def _zPosChanged(self, *a):
"""Called when the :attr:`.SliceCanvas.pos` ``z`` value changes.
Makes sure that the corresponding slice is visible.
"""
# figure out where we are in the canvas world
opts = self.opts
canvasX, canvasY = self.worldToCanvas(opts.pos.xyz)
# Get the actual canvas bounds
xlo, xhi, ylo, yhi = self._realBounds
# already in bounds
if canvasX >= xlo and \
canvasX <= xhi and \
canvasY >= ylo and \
canvasY <= yhi:
return
# figure out what row we're on
zpos = opts.pos[opts.zax]
sliceno = int(np.floor((zpos - opts.zrange.xlo) / opts.sliceSpacing))
row = int(np.floor(sliceno / opts.ncols))
# and make sure that row is visible
opts.topRow = row
[docs] def _overlayListChanged(self, *a):
"""Overrides :meth:`.SliceCanvas._overlayListChanged`.
Regenerates slice locations for all overlays, and calls the
:meth:`.SliceCanvas._overlayListChanged` method.
"""
self._updateZAxisProperties()
self._genSliceLocations()
slicecanvas.SliceCanvas._overlayListChanged(self, *a)
[docs] def _updateZAxisProperties(self):
"""Called by the :meth:`_overlayListChanged` and
:meth:`_overlayBoundsChanged` methods.
Updates the constraints (minimum/maximum values) of the
:attr:`sliceSpacing` and :attr:`zrange` properties.
"""
opts = self.opts
if len(self.overlayList) == 0:
opts.setAttribute('zrange', 'minDistance', 0)
opts.zrange.x = (0, 0)
opts.sliceSpacing = 0
return
# Get the new Z range from the
# display context bounding box.
#
# And calculate the minimum possible
# slice spacing - the smallest pixdim
# across all overlays in the list.
newZRange = self.displayCtx.bounds.getRange(opts.zax)
newZGap = sys.float_info.max
for overlay in self.overlayList:
zgap = self.calcSliceSpacing(overlay)
if zgap < newZGap:
newZGap = zgap
# Update the zrange and slice
# spacing constraints
opts.zrange.setLimits(0, *newZRange)
opts.setAttribute('zrange', 'minDistance', newZGap)
opts.setAttribute('sliceSpacing', 'minval', newZGap)
[docs] def _overlayBoundsChanged(self, *a):
"""Overrides :meth:`.SliceCanvas._overlayBoundsChanged`.
Called when the :attr:`.DisplayContext.bounds` change. Updates the
:attr:`zrange` min/max values.
"""
slicecanvas.SliceCanvas._overlayBoundsChanged(self, preserveZoom=False)
self._updateZAxisProperties()
self._calcNumSlices()
self._genSliceLocations()
[docs] def _updateDisplayBounds(self, *args, **kwargs):
"""Overrides :meth:`.SliceCanvas._updateDisplayBounds`.
Called on canvas resizes, display bound changes and lightbox slice
property changes. Calculates the required bounding box that is to
be displayed, in display coordinates.
"""
if self.destroyed:
return
opts = self.opts
xmin = self.displayCtx.bounds.getLo( opts.xax)
ymin = self.displayCtx.bounds.getLo( opts.yax)
xlen = self.displayCtx.bounds.getLen(opts.xax)
ylen = self.displayCtx.bounds.getLen(opts.yax)
# Calculate the vertical offset required to
# ensure that the current 'topRow' is the first
# row, and the correct number of rows ('nrows')
# are displayed
# if the number of rows to be displayed (nrows)
# is more than the number of rows that exist
# (totalRows), calculate an offset to vertically
# centre the existing row space in the display
# row space
if self._totalRows < opts.nrows:
off = (self._totalRows - opts.nrows) / 2.0
# otherwise calculate the offset so that the
# top of the display space lines up with the
# current topRow
else:
off = self._totalRows - opts.nrows - opts.topRow
ymin = ymin + ylen * off
xmax = xmin + xlen * opts.ncols
ymax = ymin + ylen * opts.nrows
# Save the real canvas bounds
self._realBounds = (xmin, xmax, ymin, ymax)
log.debug('Required lightbox bounds: X: ({}, {}) Y: ({}, {})'.format(
xmin, xmax, ymin, ymax))
slicecanvas.SliceCanvas._updateDisplayBounds(
self, (xmin, xmax, ymin, ymax))
[docs] def _genSliceLocations(self):
"""Called when any of the slice display properties change.
For every overlay in the overlay list, generates a list of
transformation matrices, and a list of slice indices. The latter
specifies the slice indices from the overlay to be displayed, and the
former specifies the transformation matrix to be used to position the
slice on the canvas.
"""
# calculate the locations, in display coordinates,
# of all slices to be displayed on the canvas
opts = self.opts
sliceLocs = np.arange(
opts.zrange.xlo + opts.sliceSpacing * 0.5,
opts.zrange.xhi,
opts.sliceSpacing)
self._sliceLocs = {}
self._transforms = {}
# calculate the transformation for each
# slice in each overlay, and the index of
# each slice to be displayed
for i, overlay in enumerate(self.overlayList):
iSliceLocs = []
iTransforms = []
for zi, zpos in enumerate(sliceLocs):
xform = self._calculateSliceTransform(overlay, zi)
iTransforms.append(xform)
iSliceLocs .append(zpos)
self._transforms[overlay] = iTransforms
self._sliceLocs[ overlay] = iSliceLocs
def __prepareSliceTransforms(self, globj, xforms):
"""Applies the :attr:`.SliceCanvas.invertX` and
:attr:`.SliceCanvas.invertY` properties to the given transformation
matrices, if necessary. Returns the transformations.
"""
opts = self.opts
if not opts.invertX or opts.invertY:
return xforms
invXforms = []
lo, hi = globj.getDisplayBounds()
xmin = lo[opts.xax]
xmax = hi[opts.xax]
ymin = lo[opts.yax]
ymax = hi[opts.yax]
xlen = xmax - xmin
ylen = ymax - ymin
# We have to translate each slice transformation
# to the origin, perform the flip there, then
# transform it back to its original location.
for xform in xforms:
invert = np.eye(4)
toOrigin = np.eye(4)
fromOrigin = np.eye(4)
xoff = xlen / 2.0 + xform[opts.xax, 3] + xmin
yoff = ylen / 2.0 + xform[opts.yax, 3] + ymin
if opts.invertX:
invert[ opts.xax, opts.xax] = -1
toOrigin[ opts.xax, 3] = -xoff
fromOrigin[opts.xax, 3] = xoff
if opts.invertY:
invert[ opts.yax, opts.yax] = -1
toOrigin[ opts.yax, 3] = -yoff
fromOrigin[opts.yax, 3] = yoff
xform = affine.concat(fromOrigin, invert, toOrigin, xform)
invXforms.append(xform)
return invXforms
[docs] def _drawGridLines(self):
"""Draws grid lines between all the displayed slices."""
opts = self.opts
if self._totalRows == 0 or opts.ncols == 0:
return
xlen = self.displayCtx.bounds.getLen(opts.xax)
ylen = self.displayCtx.bounds.getLen(opts.yax)
xmin = self.displayCtx.bounds.getLo( opts.xax)
ymin = self.displayCtx.bounds.getLo( opts.yax)
rowLines = np.zeros(((self._totalRows - 1) * 2, 2), dtype=np.float32)
colLines = np.zeros(((opts.ncols - 1) * 2, 2), dtype=np.float32)
topRow = self._totalRows - opts.topRow
btmRow = topRow - self._totalRows
rowLines[:, 1] = np.arange(
ymin + (btmRow + 1) * ylen,
ymin + topRow * ylen, ylen).repeat(2)
rowLines[:, 0] = np.tile(
np.array([xmin, xmin + opts.ncols * xlen]),
self._totalRows - 1)
colLines[:, 0] = np.arange(
xmin + xlen,
xmin + xlen * opts.ncols, xlen).repeat(2)
colLines[:, 1] = np.tile(np.array([
ymin + btmRow * ylen,
ymin + topRow * ylen]), opts.ncols - 1)
colour = (0.3, 0.9, 1.0, 0.8)
for lines in (rowLines, colLines):
for i in range(0, len(lines), 2):
self.getAnnotations().line(lines[i],
lines[i + 1],
colour=colour,
width=2)
[docs] def _drawSliceHighlight(self):
"""Draws a box around the slice which contains the current cursor
location.
"""
opts = self.opts
zpos = opts.pos[opts.zax]
sliceno = int(np.floor((zpos - opts.zrange.xlo) / opts.sliceSpacing))
xlen = self.displayCtx.bounds.getLen(opts.xax)
ylen = self.displayCtx.bounds.getLen(opts.yax)
xmin = self.displayCtx.bounds.getLo( opts.xax)
ymin = self.displayCtx.bounds.getLo( opts.yax)
row = int(np.floor(sliceno / opts.ncols))
col = int(np.floor(sliceno % opts.ncols))
# don't draw the cursor if it is on a
# non-existent or non-displayed slice
if sliceno > self._nslices: return
if row < opts.topRow: return
if row >= opts.topRow + opts.nrows: return
# in GL space, the top row is actually the bottom row
row = self._totalRows - row - 1
self.getAnnotations().rect((xmin + xlen * col,
ymin + ylen * row),
xlen,
ylen,
colour=(1, 0, 0),
width=2)
[docs] def _drawCursor(self):
"""Draws a cursor at the current canvas position (the
:attr:`.SliceCanvas.pos` property).
"""
opts = self.opts
zpos = opts.pos[opts.zax]
sliceno = int(np.floor((zpos - opts.zrange.xlo) / opts.sliceSpacing))
xlen = self.displayCtx.bounds.getLen(opts.xax)
ylen = self.displayCtx.bounds.getLen(opts.yax)
xmin = self.displayCtx.bounds.getLo( opts.xax)
ymin = self.displayCtx.bounds.getLo( opts.yax)
row = int(np.floor(sliceno / opts.ncols))
col = int(np.floor(sliceno % opts.ncols))
# don't draw the cursor if it is on a
# non-existent or non-displayed slice
if sliceno > self._nslices: return
if row < opts.topRow: return
if row >= opts.topRow + opts.nrows: return
# in GL space, the top row is actually the bottom row
row = self._totalRows - row - 1
xpos, ypos = self.worldToCanvas(opts.pos.xyz)
xverts = np.zeros((2, 2))
yverts = np.zeros((2, 2))
xverts[:, 0] = xpos
xverts[0, 1] = ymin + (row) * ylen
xverts[1, 1] = ymin + (row + 1) * ylen
yverts[:, 1] = ypos
yverts[0, 0] = xmin + (col) * xlen
yverts[1, 0] = xmin + (col + 1) * xlen
annot = self.getAnnotations()
kwargs = {
'colour' : opts.cursorColour,
'width' : 1
}
annot.line(xverts[0], xverts[1], **kwargs)
annot.line(yverts[0], yverts[1], **kwargs)
[docs] def _draw(self, *a):
"""Draws the current scene to the canvas. """
if self.destroyed:
return
width, height = self.GetSize()
if width == 0 or height == 0:
return
if not self._setGLContext():
return
opts = self.opts
axes = (opts.xax, opts.yax, opts.zax)
overlays, globjs = self._getGLObjects()
# See comment in SliceCanvas._draw
# regarding this test
if any([not g.ready() for g in globjs]):
return
if opts.renderMode == 'offscreen':
log.debug('Rendering to off-screen texture')
rt = self._offscreenRenderTexture
lo = [None] * 3
hi = [None] * 3
lo[opts.xax], hi[opts.xax] = opts.displayBounds.x
lo[opts.yax], hi[opts.yax] = opts.displayBounds.y
lo[opts.zax], hi[opts.zax] = opts.zrange
rt.bindAsRenderTarget()
rt.setRenderViewport(opts.xax, opts.yax, lo, hi)
glroutines.clear((0, 0, 0, 0))
else:
self._setViewport(invertX=False, invertY=False)
glroutines.clear(opts.bgColour)
startSlice = opts.ncols * opts.topRow
endSlice = startSlice + opts.nrows * opts.ncols
if endSlice > self._nslices:
endSlice = self._nslices
# Draw all the slices for all the overlays.
for overlay, globj in zip(overlays, globjs):
display = self.displayCtx.getDisplay(overlay)
globj = self._glObjects.get(overlay, None)
if not display.enabled:
continue
log.debug('Drawing {} slices ({} - {}) for '
'overlay {} directly to canvas'.format(
endSlice - startSlice,
startSlice,
endSlice,
overlay))
zposes = self._sliceLocs[ overlay][startSlice:endSlice]
xforms = self._transforms[overlay][startSlice:endSlice]
xforms = self.__prepareSliceTransforms(globj, xforms)
if opts.renderMode == 'prerender':
rt, name = self._prerenderTextures.get(overlay, (None, None))
if rt is None:
continue
log.debug('Drawing {} slices ({} - {}) for overlay {} '
'from pre-rendered texture'.format(
endSlice - startSlice,
startSlice,
endSlice,
overlay))
for zpos, xform in zip(zposes, xforms):
rt.draw(zpos, xform)
else:
globj.preDraw()
globj.drawAll(axes, zposes, xforms)
globj.postDraw()
if opts.renderMode == 'offscreen':
rt.unbindAsRenderTarget()
rt.restoreViewport()
self._setViewport(invertX=False, invertY=False)
glroutines.clear(opts.bgColour)
rt.drawOnBounds(
0,
opts.displayBounds.xlo,
opts.displayBounds.xhi,
opts.displayBounds.ylo,
opts.displayBounds.yHi,
opts.xax,
opts.yax)
if len(self.overlayList) > 0:
if opts.showCursor: self._drawCursor()
if opts.showGridLines: self._drawGridLines()
if opts.highlightSlice: self._drawSliceHighlight()
self.getAnnotations().draw(opts.pos[opts.zax])