Source code for fsleyes.gl.globject

#
# globject.py - The GLObject class.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`GLObject` class, which is a superclass
for all FSLeyes OpenGL overlay types. The following classes are
defined in this module:

.. autosummary::
   :nosignatures:

   GLObject
   GLSimpleObject


See also the :class:`.GLImageObject`, which is the base class for all
``GLObject`` sub-types that display :class:`.Nifti` overlays.


This module also provides a few functions, most importantly
:func:`createGLObject`:

.. autosummary::
   :nosignatures:

   getGLObjectType
   createGLObject
"""

import logging

import fsl.utils.notifier as notifier


log = logging.getLogger(__name__)


[docs]def getGLObjectType(overlayType): """This function returns an appropriate :class:`GLObject` type for the given :attr:`.Display.overlayType` value. """ from . import glvolume from . import glrgbvolume from . import glcomplex from . import glmask from . import glrgbvector from . import gllinevector from . import glmesh from . import gllabel from . import gltensor from . import glsh from . import glmip typeMap = { 'volume' : glvolume .GLVolume, 'mask' : glmask .GLMask, 'rgbvector' : glrgbvector .GLRGBVector, 'linevector' : gllinevector.GLLineVector, 'mesh' : glmesh .GLMesh, 'label' : gllabel .GLLabel, 'tensor' : gltensor .GLTensor, 'sh' : glsh .GLSH, 'mip' : glmip .GLMIP, 'rgb' : glrgbvolume .GLRGBVolume, 'complex' : glcomplex .GLComplex } return typeMap.get(overlayType, None)
[docs]def createGLObject(overlay, overlayList, displayCtx, canvas, threedee=False): """Create :class:`GLObject` instance for the given overlay, as specified by the :attr:`.Display.overlayType` property. :arg overlay: An overlay object (e.g. a :class:`.Image` instance). :arg overlayList: The :class:`.OverlayList` :arg displayCtx: The :class:`.DisplayContext` managing the scene. :arg canvas: The canvas which will be displaying this ``GLObject``. :arg threedee: If ``True``, the ``GLObject`` will be configured for 3D rendering. Otherwise it will be configured for 2D slice-based rendering. """ display = displayCtx.getDisplay(overlay) ctr = getGLObjectType(display.overlayType) if ctr is not None: return ctr(overlay, overlayList, displayCtx, canvas, threedee) else: return None
[docs]class GLObject(notifier.Notifier): """The :class:`GLObject` class is a base class for all OpenGL objects displayed in *FSLeyes*. **Instance attributes** The following attributes will always be available on ``GLObject`` instances: - ``name``: A unique name for this ``GLObject`` instance. - ``overlay``: The overlay to be displayed. - ``display``: The :class:`.Display` instance describing the overlay display properties. - ``opts``: The :class:`.DisplayOpts` instance describing the overlay-type specific display properties. - ``displayCtx``: The :class:`.DisplayContext` managing the scene that this ``GLObject`` is a part of. - ``canvas``: The canvas which is displaying this ``GLObject``. Could be a :class:`.SliceCanvas`, a :class:`.LightBoxCanvas`, a :class:`.Scene3DCanvas`, or some future not-yet-created canvas. - ``threedee``: A boolean flag indicating whether this ``GLObject`` is configured for 2D or 3D rendering. **Usage** Once you have created a ``GLObject``: 1. Do not use the ``GLObject`` until its :meth:`ready` method returns ``True``. 2. In order to render the ``GLObject`` to a canvas, call (in order) the :meth:`preDraw`, :meth:`draw2D` (or :meth:`draw3D`), and :meth:`postDraw`, methods. Multple calls to :meth:`draw2D`/:meth:`draw3D` may occur between calls to :meth:`preDraw` and :meth:`postDraw`. 3. Once you are finished with the ``GLObject``, call its :meth:`destroy` method. Note that a ``GLObject`` which has been created for 2D rendering is not expected be able to render in 3D, nor vice-versa. **Update listeners** A ``GLObject`` instance will notify registered listeners when its state changes and it needs to be re-drawn. Entities which are interested in changes to a ``GLObject`` instance may register as *update listeners*, via the :meth:`.Notifier.register` method. It is the resposibility of sub-classes of ``GLObject`` to call the :meth:`.Notifier.notify` method to facilitate this notification process. **Sub-class resposibilities*** Sub-class implementations must do the following: - Call :meth:`__init__`. A ``GLObject.__init__`` sub-class method must have the following signature, and must pass all arguments through to ``GLObject.__init__``:: def __init__(self, overlay, displayCtx, canvas, threedee) - Call :meth:`notify` whenever its OpenGL representation changes. - Override the following methods: .. autosummary:: :nosignatures: getDisplayBounds getDataResolution ready destroy destroyed preDraw draw2D draw3D postDraw Alternately, a sub-class could derive from one of the following classes, instead of deriving directly from the ``GLObject`` class: .. autosummary:: :nosignatures: GLSimpleObject .GLImageObject """
[docs] def __init__(self, overlay, overlayList, displayCtx, canvas, threedee): """Create a :class:`GLObject`. The constructor adds one attribute to this instance, ``name``, which is simply a unique name for this instance. Subclass implementations must call this method, and should also perform any necessary OpenGL initialisation, such as creating textures. :arg overlay: The overlay :arg overlayList: The :class:`.OverlayList` :arg displayCtx: The ``DisplayContext`` managing the scene :arg canvas: The canvas that is displaying this ``GLObject``. :arg threedee: Whether this ``GLObject`` is to be used for 2D or 3D rendering. """ self.__name = '{}_{}'.format(type(self).__name__, id(self)) self.__threedee = threedee self.__overlay = overlay self.__overlayList = overlayList self.__displayCtx = displayCtx self.__canvas = canvas self.__display = None self.__opts = None # GLSimpleObject passes in None for # both the overlay and the displayCtx. if overlay is not None and displayCtx is not None: self.__display = displayCtx.getDisplay(overlay) self.__opts = self.__display.opts log.debug('{}.init ({})'.format(type(self).__name__, id(self)))
[docs] def __del__(self): """Prints a log message.""" if log: log.debug('{}.del ({})'.format(type(self).__name__, id(self)))
@property def name(self): """A unique name for this ``GLObject``. """ return self.__name @property def overlay(self): """The overlay being drawn by this ``GLObject``.""" return self.__overlay @property def canvas(self): """The canvas which is drawing this ``GLObject``.""" return self.__canvas @property def display(self): """The :class:`.Display` instance containing overlay display properties. """ return self.__display @property def opts(self): """The :class:`.DisplayOpts` instance containing overlay (type-specific) display properties. """ return self.__opts @property def overlayList(self): """The :class:`.OverlayList`.""" return self.__overlayList @property def displayCtx(self): """The :class:`.DisplayContext` dsecribing thef scene that this ``GLObject`` is a part of. """ return self.__displayCtx @property def threedee(self): """Property which is ``True`` if this ``GLObject`` was configured for 3D rendering, or ``False`` if it was configured for 2D slice rendering. """ return self.__threedee
[docs] def ready(self): """This method must return ``True`` or ``False`` to indicate whether this ``GLObject`` is ready to be drawn. The method should, for example, make sure that all :class:`.ImageTexture` objects are ready to be used. """ raise NotImplementedError('The ready method must be ' 'implemented by GLObject subclasses')
[docs] def getDisplayBounds(self): """This method must calculate and return a bounding box, in the display coordinate system, which contains the entire ``GLObject``. The bounds must be returned as a tuple with the following structure:: ((xlo, ylo, zlo), (xhi, yhi, zhi)) This method must be implemented by sub-classes. """ raise NotImplementedError('The getDisplayBounds method must be ' 'implemented by GLObject subclasses')
[docs] def getBoundsLengths(self): """Convenience method which returns a tuple containing the ``(x, y, z)`` lengths of the bounding box which contains the ``GLObject``. """ los, his = self.getDisplayBounds() return tuple([hi - lo for lo, hi in zip(los, his)])
[docs] def getDataResolution(self, xax, yax): """This method must calculate and return a sequence of three values, which defines a suitable pixel resolution, along the display coordinate system ``(x, y, z)`` axes, for rendering a 2D slice of this ``GLObject`` to screen. This method should be implemented by sub-classes. If not implemented, a default resolution is used. The returned resolution *might* be used to render this ``GLObject``, but typically only in a low performance environment where off-screen rendering to a :class:`.GLObjectRenderTexture` is used - see the :class:`.SliceCanvas` documentation for more details. :arg xax: Axis to be used as the horizontal screen axis. :arg yax: Axis to be used as the vertical screen axis. """ return None
[docs] def destroy(self): """This method must be called when this :class:`GLObject` is no longer needed. It should perform any necessary cleaning up, such as deleting texture objects. .. note:: Sub-classes which override this method must call this implementation. """ self.__overlay = None self.__display = None self.__opts = None self.__displayCtx = None
@property def destroyed(self): """This method may be called to test whether a call has been made to :meth:`destroy`. It should return ``True`` if this ``GLObject`` has been destroyed, ``False`` otherwise. """ raise NotImplementedError()
[docs] def preDraw(self, xform=None, bbox=None): """This method is called at the start of a draw routine. It should perform any initialisation which is required before one or more calls to the :meth:`draw2D`/:meth:`draw3D` methods are made, such as binding and configuring textures. See :meth:`draw2D` for details on the ``xform`` and ``bbox`` arguments. They are only guaranteed to be passed to the ``preDraw`` method in scenarios where only a single call to ``draw2D`` or``draw3D`` is made between calls to ``preDraw`` and ``postDraw``. """ raise NotImplementedError()
[docs] def draw2D(self, zpos, axes, xform=None, bbox=None): """This method is called on ``GLObject`` instances which are configured for 2D rendering. It should draw a view of this ``GLObject`` - a 2D slice at the given Z location, which specifies the position along the screen depth axis. :arg zpos: Position along Z axis to draw. :arg axes: Tuple containing the ``(x, y, z)`` axes in the display coordinate system The ``x`` and ``y`` axes correspond to the horizontal and vertical display axes respectively, and the ``z`` to the depth. :arg xform: If provided, it must be applied to the model view transformation before drawing. :arg bbox: If provided, defines the bounding box, in the display coordinate system, which is to be displayed. Can be used as a performance hint (i.e. to limit the number of things that are rendered). """ raise NotImplementedError()
[docs] def draw3D(self, xform=None, bbox=None): """This method is called on ``GLObject`` instances which are configured for 3D rendering. It should draw a 3D view of this ``GLObject``. :arg xform: If provided, it must be applied to the model view transformation before drawing. :arg bbox: If provided, defines the bounding box, in the display coordinate system, which is to be displayed. Can be used as a performance hint (i.e. to limit the number of things that are rendered). """ raise NotImplementedError()
[docs] def drawAll(self, axes, zposes, xforms): """This is a convenience method for 2D lightboxD canvases, where multple 2D slices at different depths are drawn alongside each other. This method should do the same as multiple calls to the :meth:`draw2D` method, one for each of the Z positions and transformation matrices contained in the ``zposes`` and ``xforms`` arrays (``axes`` is fixed). In some circumstances (hint: the :class:`.LightBoxCanvas`), better performance may be achieved in combining multiple renders, rather than doing it with separate calls to :meth:`draw`. The default implementation does exactly this, so this method need only be overridden for subclasses which are able to get better performance by combining the draws. """ for (zpos, xform) in zip(zposes, xforms): self.draw2D(zpos, axes, xform)
[docs] def postDraw(self, xform=None, bbox=None): """This method is called after the :meth:`draw2D`/:meth:`draw3D` methods have been called one or more times. It should perform any necessary cleaning up, such as unbinding textures. See the :meth:`draw2D` method for details on the ``xform`` and ``bbox`` arguments. """ raise NotImplementedError()
[docs]class GLSimpleObject(GLObject): """The ``GLSimpleObject`` class is a convenience superclass for simple rendering tasks (probably fixed-function) which are not associated with a specific overlay, and require no setup or initialisation/management of GL memory or state. It is used by the :mod:`.annotations` module. All subclasses need to do is implement the :meth:`GLObject.draw2D` and :meth:`GLObject.draw3D` methods. The :mod:`.annotations` module uses the ``GLSimpleObject`` class. Subclasses should not assume that any of the other methods will ever be called. .. note:: The :attr:`GLObject.overlay`, :attr:`GLObject.display`, :attr:`GLObject.opts`, :attr:`GLObject.canvas`, :attr:`GLObject.overlayList` and :attr:`GLObject.displayCtx` properties of a ``GLSimpleObject`` are all set to ``None``. """
[docs] def __init__(self, threedee): """Create a ``GLSimpleObject``. """ GLObject.__init__(self, None, None, None, None, threedee) self.__destroyed = False
[docs] def ready(self): """Overrides :meth:`GLObject.ready`. Returns ``True``. """ return True
[docs] def destroy( self): """Overrides :meth:`GLObject.destroy`. Does nothing. """ GLObject.destroy(self) self.__destroyed = True
@property def destroyed(self): """Overrides :meth:`GLObject.destroy`. Returns ``True`` if :meth:`destroy` hs been called, ``False`` otherwise. """ return self.__destroyed
[docs] def preDraw(self, *args, **kwargs): """Overrides :meth:`GLObject.preDraw`. Does nothing. """ pass
[docs] def postDraw(self, *args, **kwargs): """Overrides :meth:`GLObject.postDraw`. Does nothing. """ pass