summaryrefslogtreecommitdiff
path: root/silx/gui/plot3d/scene/primitives.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot3d/scene/primitives.py')
-rw-r--r--silx/gui/plot3d/scene/primitives.py2524
1 files changed, 0 insertions, 2524 deletions
diff --git a/silx/gui/plot3d/scene/primitives.py b/silx/gui/plot3d/scene/primitives.py
deleted file mode 100644
index 7f35c3c..0000000
--- a/silx/gui/plot3d/scene/primitives.py
+++ /dev/null
@@ -1,2524 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-2021 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.
-#
-# ###########################################################################*/
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
-import ctypes
-from functools import reduce
-import logging
-import string
-
-import numpy
-
-from silx.gui.colors import rgba
-
-from ... import _glutils
-from ..._glutils import gl
-
-from . import event
-from . import core
-from . import transform
-from . import utils
-from .function import Colormap
-
-_logger = logging.getLogger(__name__)
-
-
-# Geometry ####################################################################
-
-class Geometry(core.Elem):
- """Set of vertices with normals and colors.
-
- :param str mode: OpenGL drawing mode:
- lines, line_strip, loop, triangles, triangle_strip, fan
- :param indices: Array of vertex indices or None
- :param bool copy: True (default) to copy the data, False to use as is.
- :param str attrib0: Name of the attribute that MUST be an array.
- :param attributes: Provide list of attributes as extra parameters.
- """
-
- _ATTR_INFO = {
- 'position': {'dims': (1, 2), 'lastDim': (2, 3, 4)},
- 'normal': {'dims': (1, 2), 'lastDim': (3,)},
- 'color': {'dims': (1, 2), 'lastDim': (3, 4)},
- }
-
- _MODE_CHECKS = { # Min, Modulo
- 'lines': (2, 2), 'line_strip': (2, 0), 'loop': (2, 0),
- 'points': (1, 0),
- 'triangles': (3, 3), 'triangle_strip': (3, 0), 'fan': (3, 0)
- }
-
- _MODES = {
- 'lines': gl.GL_LINES,
- 'line_strip': gl.GL_LINE_STRIP,
- 'loop': gl.GL_LINE_LOOP,
-
- 'points': gl.GL_POINTS,
-
- 'triangles': gl.GL_TRIANGLES,
- 'triangle_strip': gl.GL_TRIANGLE_STRIP,
- 'fan': gl.GL_TRIANGLE_FAN
- }
-
- _LINE_MODES = 'lines', 'line_strip', 'loop'
-
- _TRIANGLE_MODES = 'triangles', 'triangle_strip', 'fan'
-
- def __init__(self,
- mode,
- indices=None,
- copy=True,
- attrib0='position',
- **attributes):
- super(Geometry, self).__init__()
-
- self._attrib0 = str(attrib0)
-
- self._vbos = {} # Store current vbos
- self._unsyncAttributes = [] # Store attributes to copy to vbos
- self.__bounds = None # Cache object's bounds
- # Attribute names defining the object bounds
- self.__boundsAttributeNames = (self._attrib0,)
-
- assert mode in self._MODES
- self._mode = mode
-
- # Set attributes
- self._attributes = {}
- for name, data in attributes.items():
- self.setAttribute(name, data, copy=copy)
-
- # Set indices
- self._indices = None
- self.setIndices(indices, copy=copy)
-
- # More consistency checks
- mincheck, modulocheck = self._MODE_CHECKS[self._mode]
- if self._indices is not None:
- nbvertices = len(self._indices)
- else:
- nbvertices = self.nbVertices
-
- if nbvertices != 0:
- assert nbvertices >= mincheck
- if modulocheck != 0:
- assert (nbvertices % modulocheck) == 0
-
- @property
- def drawMode(self):
- """Kind of primitive to render, in :attr:`_MODES` (str)"""
- return self._mode
-
- @staticmethod
- def _glReadyArray(array, copy=True):
- """Making a contiguous array, checking float types.
-
- :param iterable array: array-like data to prepare for attribute
- :param bool copy: True to make a copy of the array, False to use as is
- """
- # Convert single value (int, float, numpy types) to tuple
- if not isinstance(array, abc.Iterable):
- array = (array, )
-
- # Makes sure it is an array
- array = numpy.array(array, copy=False)
-
- dtype = None
- if array.dtype.kind == 'f' and array.dtype.itemsize != 4:
- # Cast to float32
- _logger.info('Cast array to float32')
- dtype = numpy.float32
- elif array.dtype.itemsize > 4:
- # Cast (u)int64 to (u)int32
- if array.dtype.kind == 'i':
- _logger.info('Cast array to int32')
- dtype = numpy.int32
- elif array.dtype.kind == 'u':
- _logger.info('Cast array to uint32')
- dtype = numpy.uint32
-
- return numpy.array(array, dtype=dtype, order='C', copy=copy)
-
- @property
- def nbVertices(self):
- """Returns the number of vertices of current attributes.
-
- It returns None if there is no attributes.
- """
- for array in self._attributes.values():
- if len(array.shape) == 2:
- return len(array)
- return None
-
- @property
- def attrib0(self):
- """Attribute name that MUST be an array (str)"""
- return self._attrib0
-
- def setAttribute(self, name, array, copy=True):
- """Set attribute with provided array.
-
- :param str name: The name of the attribute
- :param array: Array-like attribute data or None to remove attribute
- :param bool copy: True (default) to copy the data, False to use as is
- """
- # This triggers associated GL resources to be garbage collected
- self._vbos.pop(name, None)
-
- if array is None:
- self._attributes.pop(name, None)
-
- else:
- array = self._glReadyArray(array, copy=copy)
-
- if name not in self._ATTR_INFO:
- _logger.debug('Not checking attribute %s dimensions', name)
- else:
- checks = self._ATTR_INFO[name]
-
- if (array.ndim == 1 and checks['lastDim'] == (1,) and
- len(array) > 1):
- array = array.reshape((len(array), 1))
-
- # Checks
- assert array.ndim in checks['dims'], "Attr %s" % name
- assert array.shape[-1] in checks['lastDim'], "Attr %s" % name
-
- # Makes sure attrib0 is considered as an array of values
- if name == self.attrib0 and array.ndim == 1:
- array.shape = 1, -1
-
- # Check length against another attribute array
- # Causes problems when updating
- # nbVertices = self.nbVertices
- # if array.ndim == 2 and nbVertices is not None:
- # assert len(array) == nbVertices
-
- self._attributes[name] = array
- if array.ndim == 2: # Store this in a VBO
- self._unsyncAttributes.append(name)
-
- if name in self.boundsAttributeNames: # Reset bounds
- self.__bounds = None
-
- self.notify()
-
- def getAttribute(self, name, copy=True):
- """Returns the numpy.ndarray corresponding to the name attribute.
-
- :param str name: The name of the attribute to get.
- :param bool copy: True to get a copy (default),
- False to get internal array (DO NOT MODIFY)
- :return: The corresponding array or None if no corresponding attribute.
- :rtype: numpy.ndarray
- """
- attr = self._attributes.get(name, None)
- return None if attr is None else numpy.array(attr, copy=copy)
-
- def useAttribute(self, program, name=None):
- """Enable and bind attribute(s) for a specific program.
-
- This MUST be called with OpenGL context active and after prepareGL2
- has been called.
-
- :param GLProgram program: The program for which to set the attributes
- :param str name: The attribute name to set or None to set then all
- """
- if name is None:
- for name in program.attributes:
- self.useAttribute(program, name)
-
- else:
- attribute = program.attributes.get(name)
- if attribute is None:
- return
-
- vboattrib = self._vbos.get(name)
- if vboattrib is not None:
- gl.glEnableVertexAttribArray(attribute)
- vboattrib.setVertexAttrib(attribute)
-
- elif name not in self._attributes:
- gl.glDisableVertexAttribArray(attribute)
-
- else:
- array = self._attributes[name]
- assert array is not None
-
- if array.ndim == 1:
- assert len(array) in (1, 2, 3, 4)
- gl.glDisableVertexAttribArray(attribute)
- _glVertexAttribFunc = getattr(
- _glutils.gl, 'glVertexAttrib{}f'.format(len(array)))
- _glVertexAttribFunc(attribute, *array)
- else:
- # TODO As is this is a never event, remove?
- gl.glEnableVertexAttribArray(attribute)
- gl.glVertexAttribPointer(
- attribute,
- array.shape[-1],
- _glutils.numpyToGLType(array.dtype),
- gl.GL_FALSE,
- 0,
- array)
-
- def setIndices(self, indices, copy=True):
- """Set the primitive indices to use.
-
- :param indices: Array-like of uint primitive indices or None to unset
- :param bool copy: True (default) to copy the data, False to use as is
- """
- # Trigger garbage collection of previous indices VBO if any
- self._vbos.pop('__indices__', None)
-
- if indices is None:
- self._indices = None
- else:
- indices = self._glReadyArray(indices, copy=copy).ravel()
- assert indices.dtype.name in ('uint8', 'uint16', 'uint32')
- if _logger.getEffectiveLevel() <= logging.DEBUG:
- # This might be a costy check
- assert indices.max() < self.nbVertices
- self._indices = indices
- self.notify()
-
- def getIndices(self, copy=True):
- """Returns the numpy.ndarray corresponding to the indices.
-
- :param bool copy: True to get a copy (default),
- False to get internal array (DO NOT MODIFY)
- :return: The primitive indices array or None if not set.
- :rtype: numpy.ndarray or None
- """
- if self._indices is None:
- return None
- else:
- return numpy.array(self._indices, copy=copy)
-
- @property
- def boundsAttributeNames(self):
- """Tuple of attribute names defining the bounds of the object.
-
- Attributes name are taken in the given order to compute the
- (x, y, z) the bounding box, e.g.::
-
- geometry.boundsAttributeNames = 'position'
- geometry.boundsAttributeNames = 'x', 'y', 'z'
- """
- return self.__boundsAttributeNames
-
- @boundsAttributeNames.setter
- def boundsAttributeNames(self, names):
- self.__boundsAttributeNames = tuple(str(name) for name in names)
- self.__bounds = None
- self.notify()
-
- def _bounds(self, dataBounds=False):
- if self.__bounds is None:
- if len(self.boundsAttributeNames) == 0:
- return None # No bounds
-
- self.__bounds = numpy.zeros((2, 3), dtype=numpy.float32)
-
- # Coordinates defined in one or more attributes
- index = 0
- for name in self.boundsAttributeNames:
- if index == 3:
- _logger.error("Too many attributes defining bounds")
- break
-
- attribute = self._attributes[name]
- assert attribute.ndim in (1, 2)
- if attribute.ndim == 1: # Single value
- min_ = attribute
- max_ = attribute
- elif len(attribute) > 0: # Array of values, compute min/max
- min_ = numpy.nanmin(attribute, axis=0)
- max_ = numpy.nanmax(attribute, axis=0)
- else:
- min_, max_ = numpy.zeros((2, attribute.shape[1]), dtype=numpy.float32)
-
- toCopy = min(len(min_), 3-index)
- if toCopy != len(min_):
- _logger.error("Attribute defining bounds"
- " has too many dimensions")
-
- self.__bounds[0, index:index+toCopy] = min_[:toCopy]
- self.__bounds[1, index:index+toCopy] = max_[:toCopy]
-
- index += toCopy
-
- self.__bounds[numpy.isnan(self.__bounds)] = 0. # Avoid NaNs
-
- return self.__bounds.copy()
-
- def prepareGL2(self, ctx):
- # TODO manage _vbo and multiple GL context + allow to share them !
- # TODO make one or multiple VBO depending on len(vertices),
- # TODO use a general common VBO for small amount of data
- for name in self._unsyncAttributes:
- array = self._attributes[name]
- self._vbos[name] = ctx.glCtx.makeVboAttrib(array)
- self._unsyncAttributes = []
-
- if self._indices is not None and '__indices__' not in self._vbos:
- vbo = ctx.glCtx.makeVbo(self._indices,
- usage=gl.GL_STATIC_DRAW,
- target=gl.GL_ELEMENT_ARRAY_BUFFER)
- self._vbos['__indices__'] = vbo
-
- def _draw(self, program=None, nbVertices=None):
- """Perform OpenGL draw calls.
-
- :param GLProgram program:
- If not None, call :meth:`useAttribute` for this program.
- :param int nbVertices:
- The number of vertices to render or None to render all vertices.
- """
- if program is not None:
- self.useAttribute(program)
-
- if self._indices is None:
- if nbVertices is None:
- nbVertices = self.nbVertices
- gl.glDrawArrays(self._MODES[self._mode], 0, nbVertices)
- else:
- if nbVertices is None:
- nbVertices = self._indices.size
- with self._vbos['__indices__']:
- gl.glDrawElements(self._MODES[self._mode],
- nbVertices,
- _glutils.numpyToGLType(self._indices.dtype),
- ctypes.c_void_p(0))
-
-
-# Lines #######################################################################
-
-class Lines(Geometry):
- """A set of segments"""
- _shaders = ("""
- attribute vec3 position;
- attribute vec3 normal;
- attribute vec4 color;
-
- uniform mat4 matrix;
- uniform mat4 transformMat;
-
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec3 vNormal;
- varying vec4 vColor;
-
- void main(void)
- {
- gl_Position = matrix * vec4(position, 1.0);
- vCameraPosition = transformMat * vec4(position, 1.0);
- vPosition = position;
- vNormal = normal;
- vColor = color;
- }
- """,
- string.Template("""
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec3 vNormal;
- varying vec4 vColor;
-
- $sceneDecl
- $lightingFunction
-
- void main(void)
- {
- $scenePreCall(vCameraPosition);
- gl_FragColor = $lightingCall(vColor, vPosition, vNormal);
- $scenePostCall(vCameraPosition);
- }
- """))
-
- def __init__(self, positions, normals=None, colors=(1., 1., 1., 1.),
- indices=None, mode='lines', width=1.):
- if mode == 'strip':
- mode = 'line_strip'
- assert mode in self._LINE_MODES
-
- self._width = width
- self._smooth = True
-
- super(Lines, self).__init__(mode, indices,
- position=positions,
- normal=normals,
- color=colors)
-
- width = event.notifyProperty('_width', converter=float,
- doc="Width of the line in pixels.")
-
- smooth = event.notifyProperty(
- '_smooth',
- converter=bool,
- doc="Smooth line rendering enabled (bool, default: True)")
-
- def renderGL2(self, ctx):
- # Prepare program
- isnormals = 'normal' in self._attributes
- if isnormals:
- fraglightfunction = ctx.viewport.light.fragmentDef
- else:
- fraglightfunction = ctx.viewport.light.fragmentShaderFunctionNoop
-
- fragment = self._shaders[1].substitute(
- sceneDecl=ctx.fragDecl,
- scenePreCall=ctx.fragCallPre,
- scenePostCall=ctx.fragCallPost,
- lightingFunction=fraglightfunction,
- lightingCall=ctx.viewport.light.fragmentCall)
- prog = ctx.glCtx.prog(self._shaders[0], fragment)
- prog.use()
-
- if isnormals:
- ctx.viewport.light.setupProgram(ctx, prog)
-
- gl.glLineWidth(self.width)
-
- prog.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- prog.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
-
- ctx.setupProgram(prog)
-
- with gl.enabled(gl.GL_LINE_SMOOTH, self._smooth):
- self._draw(prog)
-
-
-class DashedLines(Lines):
- """Set of dashed lines
-
- This MUST be defined as a set of lines (no strip or loop).
- """
-
- _shaders = ("""
- attribute vec3 position;
- attribute vec3 origin;
- attribute vec3 normal;
- attribute vec4 color;
-
- uniform mat4 matrix;
- uniform mat4 transformMat;
- uniform vec2 viewportSize; /* Width, height of the viewport */
-
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec3 vNormal;
- varying vec4 vColor;
- varying vec2 vOriginFragCoord;
-
- void main(void)
- {
- gl_Position = matrix * vec4(position, 1.0);
- vCameraPosition = transformMat * vec4(position, 1.0);
- vPosition = position;
- vNormal = normal;
- vColor = color;
-
- vec4 clipOrigin = matrix * vec4(origin, 1.0);
- vec4 ndcOrigin = clipOrigin / clipOrigin.w; /* Perspective divide */
- /* Convert to same frame as gl_FragCoord: lower-left, pixel center at 0.5, 0.5 */
- vOriginFragCoord = (ndcOrigin.xy + vec2(1.0, 1.0)) * 0.5 * viewportSize + vec2(0.5, 0.5);
- }
- """, # noqa
- string.Template("""
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec3 vNormal;
- varying vec4 vColor;
- varying vec2 vOriginFragCoord;
-
- uniform vec2 dash;
-
- $sceneDecl
- $lightingFunction
-
- void main(void)
- {
- $scenePreCall(vCameraPosition);
-
- /* Discard off dash fragments */
- float lineDist = distance(vOriginFragCoord, gl_FragCoord.xy);
- if (mod(lineDist, dash.x + dash.y) > dash.x) {
- discard;
- }
- gl_FragColor = $lightingCall(vColor, vPosition, vNormal);
-
- $scenePostCall(vCameraPosition);
- }
- """))
-
- def __init__(self, positions, colors=(1., 1., 1., 1.),
- indices=None, width=1.):
- self._dash = 1, 0
- super(DashedLines, self).__init__(positions=positions,
- colors=colors,
- indices=indices,
- mode='lines',
- width=width)
-
- @property
- def dash(self):
- """Dash of the line as a 2-tuple of lengths in pixels: (on, off)"""
- return self._dash
-
- @dash.setter
- def dash(self, dash):
- dash = float(dash[0]), float(dash[1])
- if dash != self._dash:
- self._dash = dash
- self.notify()
-
- def getPositions(self, copy=True):
- """Get coordinates of lines.
-
- :param bool copy: True to get a copy, False otherwise
- :returns: Coordinates of lines
- :rtype: numpy.ndarray of float32 of shape (N, 2, Ndim)
- """
- return self.getAttribute('position', copy=copy)
-
- def setPositions(self, positions, copy=True):
- """Set line coordinates.
-
- :param positions: Array of line coordinates
- :param bool copy: True to copy input array, False to use as is
- """
- self.setAttribute('position', positions, copy=copy)
- # Update line origins from given positions
- origins = numpy.array(positions, copy=True, order='C')
- origins[1::2] = origins[::2]
- self.setAttribute('origin', origins, copy=False)
-
- def renderGL2(self, context):
- # Prepare program
- isnormals = 'normal' in self._attributes
- if isnormals:
- fraglightfunction = context.viewport.light.fragmentDef
- else:
- fraglightfunction = \
- context.viewport.light.fragmentShaderFunctionNoop
-
- fragment = self._shaders[1].substitute(
- sceneDecl=context.fragDecl,
- scenePreCall=context.fragCallPre,
- scenePostCall=context.fragCallPost,
- lightingFunction=fraglightfunction,
- lightingCall=context.viewport.light.fragmentCall)
- program = context.glCtx.prog(self._shaders[0], fragment)
- program.use()
-
- if isnormals:
- context.viewport.light.setupProgram(context, program)
-
- gl.glLineWidth(self.width)
-
- program.setUniformMatrix('matrix', context.objectToNDC.matrix)
- program.setUniformMatrix('transformMat',
- context.objectToCamera.matrix,
- safe=True)
-
- gl.glUniform2f(
- program.uniforms['viewportSize'], *context.viewport.size)
- gl.glUniform2f(program.uniforms['dash'], *self.dash)
-
- context.setupProgram(program)
-
- self._draw(program)
-
-
-class Box(core.PrivateGroup):
- """Rectangular box"""
-
- _lineIndices = numpy.array((
- (0, 1), (1, 2), (2, 3), (3, 0), # Lines with z=0
- (0, 4), (1, 5), (2, 6), (3, 7), # Lines from z=0 to z=1
- (4, 5), (5, 6), (6, 7), (7, 4)), # Lines with z=1
- dtype=numpy.uint8)
-
- _faceIndices = numpy.array(
- (0, 3, 1, 2, 5, 6, 4, 7, 7, 6, 6, 2, 7, 3, 4, 0, 5, 1),
- dtype=numpy.uint8)
-
- _vertices = numpy.array((
- # Corners with z=0
- (0., 0., 0.), (1., 0., 0.), (1., 1., 0.), (0., 1., 0.),
- # Corners with z=1
- (0., 0., 1.), (1., 0., 1.), (1., 1., 1.), (0., 1., 1.)),
- dtype=numpy.float32)
-
- def __init__(self, stroke=(1., 1., 1., 1.), fill=(1., 1., 1., 0.)):
- super(Box, self).__init__()
-
- self._fill = Mesh3D(self._vertices,
- colors=rgba(fill),
- mode='triangle_strip',
- indices=self._faceIndices)
- self._fill.visible = self.fillColor[-1] != 0.
-
- self._stroke = Lines(self._vertices,
- indices=self._lineIndices,
- colors=rgba(stroke),
- mode='lines')
- self._stroke.visible = self.strokeColor[-1] != 0.
- self.strokeWidth = 1.
-
- self._children = [self._stroke, self._fill]
-
- self._size = 1., 1., 1.
-
- @classmethod
- def getLineIndices(cls, copy=True):
- """Returns 2D array of Box lines indices
-
- :param copy: True (default) to get a copy,
- False to get internal array (Do not modify!)
- :rtype: numpy.ndarray
- """
- return numpy.array(cls._lineIndices, copy=copy)
-
- @classmethod
- def getVertices(cls, copy=True):
- """Returns 2D array of Box corner coordinates.
-
- :param copy: True (default) to get a copy,
- False to get internal array (Do not modify!)
- :rtype: numpy.ndarray
- """
- return numpy.array(cls._vertices, copy=copy)
-
- @property
- def size(self):
- """Size of the box (sx, sy, sz)"""
- return self._size
-
- @size.setter
- def size(self, size):
- assert len(size) == 3
- size = tuple(size)
- if size != self.size:
- self._size = size
- self._fill.setAttribute(
- 'position',
- self._vertices * numpy.array(size, dtype=numpy.float32))
- self._stroke.setAttribute(
- 'position',
- self._vertices * numpy.array(size, dtype=numpy.float32))
- self.notify()
-
- @property
- def strokeSmooth(self):
- """True to draw smooth stroke, False otherwise"""
- return self._stroke.smooth
-
- @strokeSmooth.setter
- def strokeSmooth(self, smooth):
- smooth = bool(smooth)
- if smooth != self._stroke.smooth:
- self._stroke.smooth = smooth
- self.notify()
-
- @property
- def strokeWidth(self):
- """Width of the stroke (float)"""
- return self._stroke.width
-
- @strokeWidth.setter
- def strokeWidth(self, width):
- width = float(width)
- if width != self.strokeWidth:
- self._stroke.width = width
- self.notify()
-
- @property
- def strokeColor(self):
- """RGBA color of the box lines (4-tuple of float in [0, 1])"""
- return tuple(self._stroke.getAttribute('color', copy=False))
-
- @strokeColor.setter
- def strokeColor(self, color):
- color = rgba(color)
- if color != self.strokeColor:
- self._stroke.setAttribute('color', color)
- # Fully transparent = hidden
- self._stroke.visible = color[-1] != 0.
- self.notify()
-
- @property
- def fillColor(self):
- """RGBA color of the box faces (4-tuple of float in [0, 1])"""
- return tuple(self._fill.getAttribute('color', copy=False))
-
- @fillColor.setter
- def fillColor(self, color):
- color = rgba(color)
- if color != self.fillColor:
- self._fill.setAttribute('color', color)
- # Fully transparent = hidden
- self._fill.visible = color[-1] != 0.
- self.notify()
-
- @property
- def fillCulling(self):
- return self._fill.culling
-
- @fillCulling.setter
- def fillCulling(self, culling):
- self._fill.culling = culling
-
-
-class Axes(Lines):
- """3D RGB orthogonal axes"""
- _vertices = numpy.array(((0., 0., 0.), (1., 0., 0.),
- (0., 0., 0.), (0., 1., 0.),
- (0., 0., 0.), (0., 0., 1.)),
- dtype=numpy.float32)
-
- _colors = numpy.array(((255, 0, 0, 255), (255, 0, 0, 255),
- (0, 255, 0, 255), (0, 255, 0, 255),
- (0, 0, 255, 255), (0, 0, 255, 255)),
- dtype=numpy.uint8)
-
- def __init__(self):
- super(Axes, self).__init__(self._vertices,
- colors=self._colors,
- width=3.)
- self._size = 1., 1., 1.
-
- @property
- def size(self):
- """Size of the axes (sx, sy, sz)"""
- return self._size
-
- @size.setter
- def size(self, size):
- assert len(size) == 3
- size = tuple(size)
- if size != self.size:
- self._size = size
- self.setAttribute(
- 'position',
- self._vertices * numpy.array(size, dtype=numpy.float32))
- self.notify()
-
-
-class BoxWithAxes(Lines):
- """Rectangular box with RGB OX, OY, OZ axes
-
- :param color: RGBA color of the box
- """
-
- _vertices = numpy.array((
- # Axes corners
- (0., 0., 0.), (1., 0., 0.),
- (0., 0., 0.), (0., 1., 0.),
- (0., 0., 0.), (0., 0., 1.),
- # Box corners with z=0
- (1., 0., 0.), (1., 1., 0.), (0., 1., 0.),
- # Box corners with z=1
- (0., 0., 1.), (1., 0., 1.), (1., 1., 1.), (0., 1., 1.)),
- dtype=numpy.float32)
-
- _axesColors = numpy.array(((1., 0., 0., 1.), (1., 0., 0., 1.),
- (0., 1., 0., 1.), (0., 1., 0., 1.),
- (0., 0., 1., 1.), (0., 0., 1., 1.)),
- dtype=numpy.float32)
-
- _lineIndices = numpy.array((
- (0, 1), (2, 3), (4, 5), # Axes lines
- (6, 7), (7, 8), # Box lines with z=0
- (6, 10), (7, 11), (8, 12), # Box lines from z=0 to z=1
- (9, 10), (10, 11), (11, 12), (12, 9)), # Box lines with z=1
- dtype=numpy.uint8)
-
- def __init__(self, color=(1., 1., 1., 1.)):
- self._color = (1., 1., 1., 1.)
- colors = numpy.ones((len(self._vertices), 4), dtype=numpy.float32)
- colors[:len(self._axesColors), :] = self._axesColors
-
- super(BoxWithAxes, self).__init__(self._vertices,
- indices=self._lineIndices,
- colors=colors,
- width=2.)
- self._size = 1., 1., 1.
- self.color = color
-
- @property
- def color(self):
- """The RGBA color to use for the box: 4 float in [0, 1]"""
- return self._color
-
- @color.setter
- def color(self, color):
- color = rgba(color)
- if color != self._color:
- self._color = color
- colors = numpy.empty((len(self._vertices), 4), dtype=numpy.float32)
- colors[:len(self._axesColors), :] = self._axesColors
- colors[len(self._axesColors):, :] = self._color
- self.setAttribute('color', colors) # Do the notification
-
- @property
- def size(self):
- """Size of the axes (sx, sy, sz)"""
- return self._size
-
- @size.setter
- def size(self, size):
- assert len(size) == 3
- size = tuple(size)
- if size != self.size:
- self._size = size
- self.setAttribute(
- 'position',
- self._vertices * numpy.array(size, dtype=numpy.float32))
- self.notify()
-
-
-class PlaneInGroup(core.PrivateGroup):
- """A plane using its parent bounds to display a contour.
-
- If plane is outside the bounds of its parent, it is not visible.
-
- Cannot set the transform attribute of this primitive.
- This primitive never has any bounds.
- """
- # TODO inherit from Lines directly?, make sure the plane remains visible?
-
- def __init__(self, point=(0., 0., 0.), normal=(0., 0., 1.)):
- super(PlaneInGroup, self).__init__()
- self._cache = None, None # Store bounds, vertices
- self._outline = None
-
- self._color = None
- self.color = 1., 1., 1., 1. # Set _color
- self._width = 2.
- self._strokeVisible = True
-
- self._plane = utils.Plane(point, normal)
- self._plane.addListener(self._planeChanged)
-
- def moveToCenter(self):
- """Place the plane at the center of the data, not changing orientation.
- """
- if self.parent is not None:
- bounds = self.parent.bounds(dataBounds=True)
- if bounds is not None:
- center = (bounds[0] + bounds[1]) / 2.
- _logger.debug('Moving plane to center: %s', str(center))
- self.plane.point = center
-
- @property
- def color(self):
- """Plane outline color (array of 4 float in [0, 1])."""
- return self._color.copy()
-
- @color.setter
- def color(self, color):
- self._color = numpy.array(color, copy=True, dtype=numpy.float32)
- if self._outline is not None:
- self._outline.setAttribute('color', self._color)
- self.notify() # This is OK as Lines are rebuild for each rendering
-
- @property
- def width(self):
- """Width of the plane stroke in pixels"""
- return self._width
-
- @width.setter
- def width(self, width):
- self._width = float(width)
- if self._outline is not None:
- self._outline.width = self._width # Sync width
-
- @property
- def strokeVisible(self):
- """Whether surrounding stroke is visible or not (bool)."""
- return self._strokeVisible
-
- @strokeVisible.setter
- def strokeVisible(self, visible):
- self._strokeVisible = bool(visible)
- if self._outline is not None:
- self._outline.visible = self._strokeVisible
-
- # Plane access
-
- @property
- def plane(self):
- """The plane parameters in the frame of the object."""
- return self._plane
-
- def _planeChanged(self, source):
- """Listener of plane changes: clear cache and notify listeners."""
- self._cache = None, None
- self.notify()
-
- # Disable some scene features
-
- @property
- def transforms(self):
- # Ready-only transforms to prevent using it
- return self._transforms
-
- def _bounds(self, dataBounds=False):
- # This is bound less as it uses the bounds of its parent.
- return None
-
- @property
- def contourVertices(self):
- """The vertices of the contour of the plane/bounds intersection."""
- parent = self.parent
- if parent is None:
- return None # No parent: no vertices
-
- bounds = parent.bounds(dataBounds=True)
- if bounds is None:
- return None # No bounds: no vertices
-
- # Check if cache is valid and return it
- cachebounds, cachevertices = self._cache
- if numpy.all(numpy.equal(bounds, cachebounds)):
- return cachevertices
-
- # Cache is not OK, rebuild it
- boxVertices = Box.getVertices(copy=True)
- boxVertices = bounds[0] + boxVertices * (bounds[1] - bounds[0])
- lineIndices = Box.getLineIndices(copy=False)
- vertices = utils.boxPlaneIntersect(
- boxVertices, lineIndices, self.plane.normal, self.plane.point)
-
- self._cache = bounds, vertices if len(vertices) != 0 else None
-
- return self._cache[1]
-
- @property
- def center(self):
- """The center of the plane/bounds intersection points."""
- if not self.isValid:
- return None
- else:
- return numpy.mean(self.contourVertices, axis=0)
-
- @property
- def isValid(self):
- """True if a contour is defined, False otherwise."""
- return self.plane.isPlane and self.contourVertices is not None
-
- def prepareGL2(self, ctx):
- if self.isValid:
- if self._outline is None: # Init outline
- self._outline = Lines(self.contourVertices,
- mode='loop',
- colors=self.color)
- self._outline.width = self._width
- self._outline.visible = self._strokeVisible
- self._children.append(self._outline)
-
- # Update vertices, TODO only when necessary
- self._outline.setAttribute('position', self.contourVertices)
-
- super(PlaneInGroup, self).prepareGL2(ctx)
-
- def renderGL2(self, ctx):
- if self.isValid:
- super(PlaneInGroup, self).renderGL2(ctx)
-
-
-class BoundedGroup(core.Group):
- """Group with data bounds"""
-
- _shape = None # To provide a default value without overriding __init__
-
- @property
- def shape(self):
- """Data shape (depth, height, width) of this group or None"""
- return self._shape
-
- @shape.setter
- def shape(self, shape):
- if shape is None:
- self._shape = None
- else:
- depth, height, width = shape
- self._shape = float(depth), float(height), float(width)
-
- @property
- def size(self):
- """Data size (width, height, depth) of this group or None"""
- shape = self.shape
- if shape is None:
- return None
- else:
- return shape[2], shape[1], shape[0]
-
- @size.setter
- def size(self, size):
- if size is None:
- self.shape = None
- else:
- self.shape = size[2], size[1], size[0]
-
- def _bounds(self, dataBounds=False):
- if dataBounds and self.size is not None:
- return numpy.array(((0., 0., 0.), self.size),
- dtype=numpy.float32)
- else:
- return super(BoundedGroup, self)._bounds(dataBounds)
-
-
-# Points ######################################################################
-
-class _Points(Geometry):
- """Base class to render a set of points."""
-
- DIAMOND = 'd'
- CIRCLE = 'o'
- SQUARE = 's'
- PLUS = '+'
- X_MARKER = 'x'
- ASTERISK = '*'
- H_LINE = '_'
- V_LINE = '|'
-
- SUPPORTED_MARKERS = (DIAMOND, CIRCLE, SQUARE, PLUS,
- X_MARKER, ASTERISK, H_LINE, V_LINE)
- """List of supported markers:
-
- - 'd' diamond
- - 'o' circle
- - 's' square
- - '+' cross
- - 'x' x-cross
- - '*' asterisk
- - '_' horizontal line
- - '|' vertical line
- """
-
- _MARKER_FUNCTIONS = {
- DIAMOND: """
- float alphaSymbol(vec2 coord, float size) {
- vec2 centerCoord = abs(coord - vec2(0.5, 0.5));
- float f = centerCoord.x + centerCoord.y;
- return clamp(size * (0.5 - f), 0.0, 1.0);
- }
- """,
- CIRCLE: """
- float alphaSymbol(vec2 coord, float size) {
- float radius = 0.5;
- float r = distance(coord, vec2(0.5, 0.5));
- return clamp(size * (radius - r), 0.0, 1.0);
- }
- """,
- SQUARE: """
- float alphaSymbol(vec2 coord, float size) {
- return 1.0;
- }
- """,
- PLUS: """
- float alphaSymbol(vec2 coord, float size) {
- vec2 d = abs(size * (coord - vec2(0.5, 0.5)));
- if (min(d.x, d.y) < 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
- }
- """,
- X_MARKER: """
- float alphaSymbol(vec2 coord, float size) {
- vec2 pos = floor(size * coord) + 0.5;
- vec2 d_x = abs(pos.x + vec2(- pos.y, pos.y - size));
- if (min(d_x.x, d_x.y) <= 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
- }
- """,
- ASTERISK: """
- float alphaSymbol(vec2 coord, float size) {
- /* Combining +, x and circle */
- vec2 d_plus = abs(size * (coord - vec2(0.5, 0.5)));
- vec2 pos = floor(size * coord) + 0.5;
- vec2 d_x = abs(pos.x + vec2(- pos.y, pos.y - size));
- if (min(d_plus.x, d_plus.y) < 0.5) {
- return 1.0;
- } else if (min(d_x.x, d_x.y) <= 0.5) {
- float r = distance(coord, vec2(0.5, 0.5));
- return clamp(size * (0.5 - r), 0.0, 1.0);
- } else {
- return 0.0;
- }
- }
- """,
- H_LINE: """
- float alphaSymbol(vec2 coord, float size) {
- float dy = abs(size * (coord.y - 0.5));
- if (dy < 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
- }
- """,
- V_LINE: """
- float alphaSymbol(vec2 coord, float size) {
- float dx = abs(size * (coord.x - 0.5));
- if (dx < 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
- }
- """
- }
-
- _shaders = (string.Template("""
- #version 120
-
- attribute float x;
- attribute float y;
- attribute float z;
- attribute $valueType value;
- attribute float size;
-
- uniform mat4 matrix;
- uniform mat4 transformMat;
-
- varying vec4 vCameraPosition;
- varying $valueType vValue;
- varying float vSize;
-
- void main(void)
- {
- vValue = value;
-
- vec4 positionVec4 = vec4(x, y, z, 1.0);
- gl_Position = matrix * positionVec4;
- vCameraPosition = transformMat * positionVec4;
-
- gl_PointSize = size;
- vSize = size;
- }
- """),
- string.Template("""
- #version 120
-
- varying vec4 vCameraPosition;
- varying float vSize;
- varying $valueType vValue;
-
- $valueToColorDecl
- $sceneDecl
- $alphaSymbolDecl
-
- void main(void)
- {
- $scenePreCall(vCameraPosition);
-
- float alpha = alphaSymbol(gl_PointCoord, vSize);
-
- gl_FragColor = $valueToColorCall(vValue);
- gl_FragColor.a *= alpha;
- if (gl_FragColor.a == 0.0) {
- discard;
- }
-
- $scenePostCall(vCameraPosition);
- }
- """))
-
- _ATTR_INFO = {
- 'x': {'dims': (1, 2), 'lastDim': (1,)},
- 'y': {'dims': (1, 2), 'lastDim': (1,)},
- 'z': {'dims': (1, 2), 'lastDim': (1,)},
- 'size': {'dims': (1, 2), 'lastDim': (1,)},
- }
-
- def __init__(self, x, y, z, value, size=1., indices=None):
- super(_Points, self).__init__('points', indices,
- x=x,
- y=y,
- z=z,
- value=value,
- size=size,
- attrib0='x')
- self.boundsAttributeNames = 'x', 'y', 'z'
- self._marker = 'o'
-
- @property
- def marker(self):
- """The marker symbol used to display the scatter plot (str)
-
- See :attr:`SUPPORTED_MARKERS` for the list of supported marker string.
- """
- return self._marker
-
- @marker.setter
- def marker(self, marker):
- marker = str(marker)
- assert marker in self.SUPPORTED_MARKERS
- if marker != self._marker:
- self._marker = marker
- self.notify()
-
- def _shaderValueDefinition(self):
- """Type definition, fragment shader declaration, fragment shader call
- """
- raise NotImplementedError(
- "This method must be implemented in subclass")
-
- def _renderGL2PreDrawHook(self, ctx, program):
- """Override in subclass to run code before calling gl draw"""
- pass
-
- def renderGL2(self, ctx):
- valueType, valueToColorDecl, valueToColorCall = \
- self._shaderValueDefinition()
- vertexShader = self._shaders[0].substitute(
- valueType=valueType)
- fragmentShader = self._shaders[1].substitute(
- sceneDecl=ctx.fragDecl,
- scenePreCall=ctx.fragCallPre,
- scenePostCall=ctx.fragCallPost,
- valueType=valueType,
- valueToColorDecl=valueToColorDecl,
- valueToColorCall=valueToColorCall,
- alphaSymbolDecl=self._MARKER_FUNCTIONS[self.marker])
- program = ctx.glCtx.prog(vertexShader, fragmentShader,
- attrib0=self.attrib0)
- program.use()
-
- gl.glEnable(gl.GL_VERTEX_PROGRAM_POINT_SIZE) # OpenGL 2
- gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2
- # gl.glEnable(gl.GL_PROGRAM_POINT_SIZE)
-
- program.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- program.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
-
- ctx.setupProgram(program)
-
- self._renderGL2PreDrawHook(ctx, program)
-
- self._draw(program)
-
-
-class Points(_Points):
- """A set of data points with an associated value and size."""
-
- _ATTR_INFO = _Points._ATTR_INFO.copy()
- _ATTR_INFO.update({'value': {'dims': (1, 2), 'lastDim': (1,)}})
-
- def __init__(self, x, y, z, value=0., size=1.,
- indices=None, colormap=None):
- super(Points, self).__init__(x=x,
- y=y,
- z=z,
- indices=indices,
- size=size,
- value=value)
-
- self._colormap = colormap or Colormap() # Default colormap
- self._colormap.addListener(self._cmapChanged)
-
- @property
- def colormap(self):
- """The colormap used to render the image"""
- return self._colormap
-
- def _cmapChanged(self, source, *args, **kwargs):
- """Broadcast colormap changes"""
- self.notify(*args, **kwargs)
-
- def _shaderValueDefinition(self):
- """Type definition, fragment shader declaration, fragment shader call
- """
- return 'float', self.colormap.decl, self.colormap.call
-
- def _renderGL2PreDrawHook(self, ctx, program):
- """Set-up colormap before calling gl draw"""
- self.colormap.setupProgram(ctx, program)
-
-
-class ColorPoints(_Points):
- """A set of points with an associated color and size."""
-
- _ATTR_INFO = _Points._ATTR_INFO.copy()
- _ATTR_INFO.update({'value': {'dims': (1, 2), 'lastDim': (3, 4)}})
-
- def __init__(self, x, y, z, color=(1., 1., 1., 1.), size=1.,
- indices=None):
- super(ColorPoints, self).__init__(x=x,
- y=y,
- z=z,
- indices=indices,
- size=size,
- value=color)
-
- def _shaderValueDefinition(self):
- """Type definition, fragment shader declaration, fragment shader call
- """
- return 'vec4', '', ''
-
- def setColor(self, color, copy=True):
- """Set colors
-
- :param color: Single RGBA color or
- 2D array of color of length number of points
- :param bool copy: True to copy colors (default),
- False to use provided array (Do not modify!)
- """
- self.setAttribute('value', color, copy=copy)
-
- def getColor(self, copy=True):
- """Returns the color or array of colors of the points.
-
- :param copy: True to get a copy (default),
- False to return internal array (Do not modify!)
- :return: Color or array of colors
- :rtype: numpy.ndarray
- """
- return self.getAttribute('value', copy=copy)
-
-
-class GridPoints(Geometry):
- # GLSL 1.30 !
- """Data points on a regular grid with an associated value and size."""
- _shaders = ("""
- #version 130
-
- in float value;
- in float size;
-
- uniform ivec3 gridDims;
- uniform mat4 matrix;
- uniform mat4 transformMat;
- uniform vec2 valRange;
-
- out vec4 vCameraPosition;
- out float vNormValue;
-
- //ivec3 coordsFromIndex(int index, ivec3 shape)
- //{
- /*Assumes that data is stored as z-major, then y, contiguous on x
- */
- // int yxPlaneSize = shape.y * shape.x; /* nb of elem in 2d yx plane */
- // int z = index / yxPlaneSize;
- // int yxIndex = index - z * yxPlaneSize; /* index in 2d yx plane */
- // int y = yxIndex / shape.x;
- // int x = yxIndex - y * shape.x;
- // return ivec3(x, y, z);
- // }
-
- ivec3 coordsFromIndex(int index, ivec3 shape)
- {
- /*Assumes that data is stored as x-major, then y, contiguous on z
- */
- int yzPlaneSize = shape.y * shape.z; /* nb of elem in 2d yz plane */
- int x = index / yzPlaneSize;
- int yzIndex = index - x * yzPlaneSize; /* index in 2d yz plane */
- int y = yzIndex / shape.z;
- int z = yzIndex - y * shape.z;
- return ivec3(x, y, z);
- }
-
- void main(void)
- {
- vNormValue = clamp((value - valRange.x) / (valRange.y - valRange.x),
- 0.0, 1.0);
-
- bool isValueInRange = value >= valRange.x && value <= valRange.y;
- if (isValueInRange) {
- /* Retrieve 3D position from gridIndex */
- vec3 coords = vec3(coordsFromIndex(gl_VertexID, gridDims));
- vec3 position = coords / max(vec3(gridDims) - 1.0, 1.0);
- gl_Position = matrix * vec4(position, 1.0);
- vCameraPosition = transformMat * vec4(position, 1.0);
- } else {
- gl_Position = vec4(2.0, 0.0, 0.0, 1.0); /* Get clipped */
- vCameraPosition = vec4(0.0, 0.0, 0.0, 0.0);
- }
-
- gl_PointSize = size;
- }
- """,
- string.Template("""
- #version 130
-
- in vec4 vCameraPosition;
- in float vNormValue;
- out vec4 gl_FragColor;
-
- $sceneDecl
-
- void main(void)
- {
- $scenePreCall(vCameraPosition);
-
- gl_FragColor = vec4(0.5 * vNormValue + 0.5, 0.0, 0.0, 1.0);
-
- $scenePostCall(vCameraPosition);
- }
- """))
-
- _ATTR_INFO = {
- 'value': {'dims': (1, 2), 'lastDim': (1,)},
- 'size': {'dims': (1, 2), 'lastDim': (1,)}
- }
-
- # TODO Add colormap, shape?
- # TODO could also use a texture to store values
-
- def __init__(self, values=0., shape=None, sizes=1., indices=None,
- minValue=None, maxValue=None):
- if isinstance(values, abc.Iterable):
- values = numpy.array(values, copy=False)
-
- # Test if gl_VertexID will overflow
- assert values.size < numpy.iinfo(numpy.int32).max
-
- self._shape = values.shape
- values = values.ravel() # 1D to add as a 1D vertex attribute
-
- else:
- assert shape is not None
- self._shape = tuple(shape)
-
- assert len(self._shape) in (1, 2, 3)
-
- super(GridPoints, self).__init__('points', indices,
- value=values,
- size=sizes)
-
- data = self.getAttribute('value', copy=False)
- self._minValue = data.min() if minValue is None else minValue
- self._maxValue = data.max() if maxValue is None else maxValue
-
- minValue = event.notifyProperty('_minValue')
- maxValue = event.notifyProperty('_maxValue')
-
- def _bounds(self, dataBounds=False):
- # Get bounds from values shape
- bounds = numpy.zeros((2, 3), dtype=numpy.float32)
- bounds[1, :] = self._shape
- bounds[1, :] -= 1
- return bounds
-
- def renderGL2(self, ctx):
- fragment = self._shaders[1].substitute(
- sceneDecl=ctx.fragDecl,
- scenePreCall=ctx.fragCallPre,
- scenePostCall=ctx.fragCallPost)
- prog = ctx.glCtx.prog(self._shaders[0], fragment)
- prog.use()
-
- gl.glEnable(gl.GL_VERTEX_PROGRAM_POINT_SIZE) # OpenGL 2
- gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2
- # gl.glEnable(gl.GL_PROGRAM_POINT_SIZE)
-
- prog.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- prog.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
-
- ctx.setupProgram(prog)
-
- gl.glUniform3i(prog.uniforms['gridDims'],
- self._shape[2] if len(self._shape) == 3 else 1,
- self._shape[1] if len(self._shape) >= 2 else 1,
- self._shape[0])
-
- gl.glUniform2f(prog.uniforms['valRange'], self.minValue, self.maxValue)
-
- self._draw(prog, nbVertices=reduce(lambda a, b: a * b, self._shape))
-
-
-# Spheres #####################################################################
-
-class Spheres(Geometry):
- """A set of spheres.
-
- Spheres are rendered as circles using points.
- This brings some limitations:
- - Do not support non-uniform scaling.
- - Assume the projection keeps ratio.
- - Do not render distorion by perspective projection.
- - If the sphere center is clipped, the whole sphere is not displayed.
- """
- # TODO check those links
- # Accounting for perspective projection
- # http://iquilezles.org/www/articles/sphereproj/sphereproj.htm
-
- # Michael Mara and Morgan McGuire.
- # 2D Polyhedral Bounds of a Clipped, Perspective-Projected 3D Sphere
- # Journal of Computer Graphics Techniques, Vol. 2, No. 2, 2013.
- # http://jcgt.org/published/0002/02/05/paper.pdf
- # https://research.nvidia.com/publication/2d-polyhedral-bounds-clipped-perspective-projected-3d-sphere
-
- # TODO some issues with small scaling and regular grid or due to sampling
-
- _shaders = ("""
- #version 120
-
- attribute vec3 position;
- attribute vec4 color;
- attribute float radius;
-
- uniform mat4 transformMat;
- uniform mat4 projMat;
- uniform vec2 screenSize;
-
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec4 vColor;
- varying float vViewDepth;
- varying float vViewRadius;
-
- void main(void)
- {
- vCameraPosition = transformMat * vec4(position, 1.0);
- gl_Position = projMat * vCameraPosition;
-
- vPosition = gl_Position.xyz / gl_Position.w;
-
- /* From object space radius to view space diameter.
- * Do not support non-uniform scaling */
- vec4 viewSizeVector = transformMat * vec4(2.0 * radius, 0.0, 0.0, 0.0);
- float viewSize = length(viewSizeVector.xyz);
-
- /* Convert to pixel size at the xy center of the view space */
- vec4 projSize = projMat * vec4(0.5 * viewSize, 0.0,
- vCameraPosition.z, vCameraPosition.w);
- gl_PointSize = max(1.0, screenSize[0] * projSize.x / projSize.w);
-
- vColor = color;
- vViewRadius = 0.5 * viewSize;
- vViewDepth = vCameraPosition.z;
- }
- """,
- string.Template("""
- # version 120
-
- uniform mat4 projMat;
-
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec4 vColor;
- varying float vViewDepth;
- varying float vViewRadius;
-
- $sceneDecl
- $lightingFunction
-
- void main(void)
- {
- $scenePreCall(vCameraPosition);
-
- /* Get normal from point coords */
- vec3 normal;
- normal.xy = 2.0 * gl_PointCoord - vec2(1.0);
- normal.y *= -1.0; /*Invert y to match NDC orientation*/
- float sqLength = dot(normal.xy, normal.xy);
- if (sqLength > 1.0) { /* Length -> out of sphere */
- discard;
- }
- normal.z = sqrt(1.0 - sqLength);
-
- /*Lighting performed in NDC*/
- /*TODO update this when lighting changed*/
- //XXX vec3 position = vPosition + vViewRadius * normal;
- gl_FragColor = $lightingCall(vColor, vPosition, normal);
-
- /*Offset depth*/
- float viewDepth = vViewDepth + vViewRadius * normal.z;
- vec2 clipZW = viewDepth * projMat[2].zw + projMat[3].zw;
- gl_FragDepth = 0.5 * (clipZW.x / clipZW.y) + 0.5;
-
- $scenePostCall(vCameraPosition);
- }
- """))
-
- _ATTR_INFO = {
- 'position': {'dims': (2, ), 'lastDim': (2, 3, 4)},
- 'radius': {'dims': (1, 2), 'lastDim': (1, )},
- 'color': {'dims': (1, 2), 'lastDim': (3, 4)},
- }
-
- def __init__(self, positions, radius=1., colors=(1., 1., 1., 1.)):
- self.__bounds = None
- super(Spheres, self).__init__('points', None,
- position=positions,
- radius=radius,
- color=colors)
-
- def renderGL2(self, ctx):
- fragment = self._shaders[1].substitute(
- sceneDecl=ctx.fragDecl,
- scenePreCall=ctx.fragCallPre,
- scenePostCall=ctx.fragCallPost,
- lightingFunction=ctx.viewport.light.fragmentDef,
- lightingCall=ctx.viewport.light.fragmentCall)
- prog = ctx.glCtx.prog(self._shaders[0], fragment)
- prog.use()
-
- ctx.viewport.light.setupProgram(ctx, prog)
-
- gl.glEnable(gl.GL_VERTEX_PROGRAM_POINT_SIZE) # OpenGL 2
- gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2
- # gl.glEnable(gl.GL_PROGRAM_POINT_SIZE)
-
- prog.setUniformMatrix('projMat', ctx.projection.matrix)
- prog.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
-
- ctx.setupProgram(prog)
-
- gl.glUniform2f(prog.uniforms['screenSize'], *ctx.viewport.size)
-
- self._draw(prog)
-
- def _bounds(self, dataBounds=False):
- if self.__bounds is None:
- self.__bounds = numpy.zeros((2, 3), dtype=numpy.float32)
- # Support vertex with to 2 to 4 coordinates
- positions = self._attributes['position']
- radius = self._attributes['radius']
- self.__bounds[0, :positions.shape[1]] = \
- (positions - radius).min(axis=0)[:3]
- self.__bounds[1, :positions.shape[1]] = \
- (positions + radius).max(axis=0)[:3]
- return self.__bounds.copy()
-
-
-# Meshes ######################################################################
-
-class Mesh3D(Geometry):
- """A conventional 3D mesh"""
-
- _shaders = ("""
- attribute vec3 position;
- attribute vec3 normal;
- attribute vec4 color;
-
- uniform mat4 matrix;
- uniform mat4 transformMat;
- //uniform mat3 matrixInvTranspose;
-
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec3 vNormal;
- varying vec4 vColor;
-
- void main(void)
- {
- vCameraPosition = transformMat * vec4(position, 1.0);
- //vNormal = matrixInvTranspose * normalize(normal);
- vPosition = position;
- vNormal = normal;
- vColor = color;
- gl_Position = matrix * vec4(position, 1.0);
- }
- """,
- string.Template("""
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec3 vNormal;
- varying vec4 vColor;
-
- $sceneDecl
- $lightingFunction
-
- void main(void)
- {
- $scenePreCall(vCameraPosition);
-
- gl_FragColor = $lightingCall(vColor, vPosition, vNormal);
-
- $scenePostCall(vCameraPosition);
- }
- """))
-
- def __init__(self,
- positions,
- colors,
- normals=None,
- mode='triangles',
- indices=None,
- copy=True):
- assert mode in self._TRIANGLE_MODES
- super(Mesh3D, self).__init__(mode, indices,
- position=positions,
- normal=normals,
- color=colors,
- copy=copy)
-
- self._culling = None
-
- @property
- def culling(self):
- """Face culling (str)
-
- One of 'back', 'front' or None.
- """
- return self._culling
-
- @culling.setter
- def culling(self, culling):
- assert culling in ('back', 'front', None)
- if culling != self._culling:
- self._culling = culling
- self.notify()
-
- def renderGL2(self, ctx):
- isnormals = 'normal' in self._attributes
- if isnormals:
- fragLightFunction = ctx.viewport.light.fragmentDef
- else:
- fragLightFunction = ctx.viewport.light.fragmentShaderFunctionNoop
-
- fragment = self._shaders[1].substitute(
- sceneDecl=ctx.fragDecl,
- scenePreCall=ctx.fragCallPre,
- scenePostCall=ctx.fragCallPost,
- lightingFunction=fragLightFunction,
- lightingCall=ctx.viewport.light.fragmentCall)
- prog = ctx.glCtx.prog(self._shaders[0], fragment)
- prog.use()
-
- if isnormals:
- ctx.viewport.light.setupProgram(ctx, prog)
-
- if self.culling is not None:
- cullFace = gl.GL_FRONT if self.culling == 'front' else gl.GL_BACK
- gl.glCullFace(cullFace)
- gl.glEnable(gl.GL_CULL_FACE)
-
- prog.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- prog.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
-
- ctx.setupProgram(prog)
-
- self._draw(prog)
-
- if self.culling is not None:
- gl.glDisable(gl.GL_CULL_FACE)
-
-
-class ColormapMesh3D(Geometry):
- """A 3D mesh with color computed from a colormap"""
-
- _shaders = ("""
- attribute vec3 position;
- attribute vec3 normal;
- attribute float value;
-
- uniform mat4 matrix;
- uniform mat4 transformMat;
- //uniform mat3 matrixInvTranspose;
-
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec3 vNormal;
- varying float vValue;
-
- void main(void)
- {
- vCameraPosition = transformMat * vec4(position, 1.0);
- //vNormal = matrixInvTranspose * normalize(normal);
- vPosition = position;
- vNormal = normal;
- vValue = value;
- gl_Position = matrix * vec4(position, 1.0);
- }
- """,
- string.Template("""
- uniform float alpha;
-
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec3 vNormal;
- varying float vValue;
-
- $colormapDecl
- $sceneDecl
- $lightingFunction
-
- void main(void)
- {
- $scenePreCall(vCameraPosition);
-
- vec4 color = $colormapCall(vValue);
- gl_FragColor = $lightingCall(color, vPosition, vNormal);
- gl_FragColor.a *= alpha;
-
- $scenePostCall(vCameraPosition);
- }
- """))
-
- def __init__(self,
- position,
- value,
- colormap=None,
- normal=None,
- mode='triangles',
- indices=None,
- copy=True):
- super(ColormapMesh3D, self).__init__(mode, indices,
- position=position,
- normal=normal,
- value=value,
- copy=copy)
-
- self._alpha = 1.0
- self._lineWidth = 1.0
- self._lineSmooth = True
- self._culling = None
- self._colormap = colormap or Colormap() # Default colormap
- self._colormap.addListener(self._cmapChanged)
-
- lineWidth = event.notifyProperty('_lineWidth', converter=float,
- doc="Width of the line in pixels.")
-
- lineSmooth = event.notifyProperty(
- '_lineSmooth',
- converter=bool,
- doc="Smooth line rendering enabled (bool, default: True)")
-
- alpha = event.notifyProperty(
- '_alpha', converter=float,
- doc="Transparency of the mesh, float in [0, 1]")
-
- @property
- def culling(self):
- """Face culling (str)
-
- One of 'back', 'front' or None.
- """
- return self._culling
-
- @culling.setter
- def culling(self, culling):
- assert culling in ('back', 'front', None)
- if culling != self._culling:
- self._culling = culling
- self.notify()
-
- @property
- def colormap(self):
- """The colormap used to render the image"""
- return self._colormap
-
- def _cmapChanged(self, source, *args, **kwargs):
- """Broadcast colormap changes"""
- self.notify(*args, **kwargs)
-
- def renderGL2(self, ctx):
- if 'normal' in self._attributes:
- self._renderGL2(ctx)
- else: # Disable lighting
- with self.viewport.light.turnOff():
- self._renderGL2(ctx)
-
- def _renderGL2(self, ctx):
- fragment = self._shaders[1].substitute(
- sceneDecl=ctx.fragDecl,
- scenePreCall=ctx.fragCallPre,
- scenePostCall=ctx.fragCallPost,
- lightingFunction=ctx.viewport.light.fragmentDef,
- lightingCall=ctx.viewport.light.fragmentCall,
- colormapDecl=self.colormap.decl,
- colormapCall=self.colormap.call)
- program = ctx.glCtx.prog(self._shaders[0], fragment)
- program.use()
-
- ctx.viewport.light.setupProgram(ctx, program)
- ctx.setupProgram(program)
- self.colormap.setupProgram(ctx, program)
-
- if self.culling is not None:
- cullFace = gl.GL_FRONT if self.culling == 'front' else gl.GL_BACK
- gl.glCullFace(cullFace)
- gl.glEnable(gl.GL_CULL_FACE)
-
- program.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- program.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
- gl.glUniform1f(program.uniforms['alpha'], self._alpha)
-
- if self.drawMode in self._LINE_MODES:
- gl.glLineWidth(self.lineWidth)
- with gl.enabled(gl.GL_LINE_SMOOTH, self.lineSmooth):
- self._draw(program)
- else:
- self._draw(program)
-
- if self.culling is not None:
- gl.glDisable(gl.GL_CULL_FACE)
-
-
-# ImageData ##################################################################
-
-class _Image(Geometry):
- """Base class for ImageData and ImageRgba"""
-
- _shaders = ("""
- attribute vec2 position;
-
- uniform mat4 matrix;
- uniform mat4 transformMat;
- uniform vec2 dataScale;
-
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec3 vNormal;
- varying vec2 vTexCoords;
-
- void main(void)
- {
- vec4 positionVec4 = vec4(position, 0.0, 1.0);
- vCameraPosition = transformMat * positionVec4;
- vPosition = positionVec4.xyz;
- vTexCoords = dataScale * position;
- gl_Position = matrix * positionVec4;
- }
- """,
- string.Template("""
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec2 vTexCoords;
- uniform sampler2D data;
- uniform float alpha;
-
- $imageDecl
- $sceneDecl
- $lightingFunction
-
- void main(void)
- {
- $scenePreCall(vCameraPosition);
-
- vec4 color = imageColor(data, vTexCoords);
- color.a *= alpha;
- if (color.a == 0.) { /* Discard fully transparent pixels */
- discard;
- }
-
- vec3 normal = vec3(0.0, 0.0, 1.0);
- gl_FragColor = $lightingCall(color, vPosition, normal);
-
- $scenePostCall(vCameraPosition);
- }
- """))
-
- _UNIT_SQUARE = numpy.array(((0., 0.), (1., 0.), (0., 1.), (1., 1.)),
- dtype=numpy.float32)
-
- def __init__(self, data, copy=True):
- super(_Image, self).__init__(mode='triangle_strip',
- position=self._UNIT_SQUARE)
-
- self._texture = None
- self._update_texture = True
- self._update_texture_filter = False
- self._data = None
- self.setData(data, copy)
- self._alpha = 1.
- self._interpolation = 'linear'
-
- self.isBackfaceVisible = True
-
- def setData(self, data, copy=True):
- assert isinstance(data, numpy.ndarray)
-
- if copy:
- data = numpy.array(data, copy=True)
-
- self._data = data
- self._update_texture = True
- # By updating the position rather than always using a unit square
- # we benefit from Geometry bounds handling
- self.setAttribute('position', self._UNIT_SQUARE * (self._data.shape[1], self._data.shape[0]))
- self.notify()
-
- def getData(self, copy=True):
- return numpy.array(self._data, copy=copy)
-
- @property
- def interpolation(self):
- """The texture interpolation mode: 'linear' or 'nearest'"""
- return self._interpolation
-
- @interpolation.setter
- def interpolation(self, interpolation):
- assert interpolation in ('linear', 'nearest')
- self._interpolation = interpolation
- self._update_texture_filter = True
- self.notify()
-
- @property
- def alpha(self):
- """Transparency of the image, float in [0, 1]"""
- return self._alpha
-
- @alpha.setter
- def alpha(self, alpha):
- self._alpha = float(alpha)
- self.notify()
-
- def _textureFormat(self):
- """Implement this method to provide texture internal format and format
-
- :return: 2-tuple of gl flags (internalFormat, format)
- """
- raise NotImplementedError(
- "This method must be implemented in a subclass")
-
- def prepareGL2(self, ctx):
- if self._texture is None or self._update_texture:
- if self._texture is not None:
- self._texture.discard()
-
- if self.interpolation == 'nearest':
- filter_ = gl.GL_NEAREST
- else:
- filter_ = gl.GL_LINEAR
- self._update_texture = False
- self._update_texture_filter = False
- if self._data.size == 0:
- self._texture = None
- else:
- internalFormat, format_ = self._textureFormat()
- self._texture = _glutils.Texture(
- internalFormat,
- self._data,
- format_,
- minFilter=filter_,
- magFilter=filter_,
- wrap=gl.GL_CLAMP_TO_EDGE)
-
- if self._update_texture_filter and self._texture is not None:
- self._update_texture_filter = False
- if self.interpolation == 'nearest':
- filter_ = gl.GL_NEAREST
- else:
- filter_ = gl.GL_LINEAR
- self._texture.minFilter = filter_
- self._texture.magFilter = filter_
-
- super(_Image, self).prepareGL2(ctx)
-
- def renderGL2(self, ctx):
- if self._texture is None:
- return # Nothing to render
-
- with self.viewport.light.turnOff():
- self._renderGL2(ctx)
-
- def _renderGL2PreDrawHook(self, ctx, program):
- """Override in subclass to run code before calling gl draw"""
- pass
-
- def _shaderImageColorDecl(self):
- """Returns fragment shader imageColor function declaration"""
- raise NotImplementedError(
- "This method must be implemented in a subclass")
-
- def _renderGL2(self, ctx):
- fragment = self._shaders[1].substitute(
- sceneDecl=ctx.fragDecl,
- scenePreCall=ctx.fragCallPre,
- scenePostCall=ctx.fragCallPost,
- lightingFunction=ctx.viewport.light.fragmentDef,
- lightingCall=ctx.viewport.light.fragmentCall,
- imageDecl=self._shaderImageColorDecl()
- )
- program = ctx.glCtx.prog(self._shaders[0], fragment)
- program.use()
-
- ctx.viewport.light.setupProgram(ctx, program)
-
- if not self.isBackfaceVisible:
- gl.glCullFace(gl.GL_BACK)
- gl.glEnable(gl.GL_CULL_FACE)
-
- program.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- program.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
- gl.glUniform1f(program.uniforms['alpha'], self._alpha)
-
- shape = self._data.shape
- gl.glUniform2f(program.uniforms['dataScale'], 1./shape[1], 1./shape[0])
-
- gl.glUniform1i(program.uniforms['data'], self._texture.texUnit)
-
- ctx.setupProgram(program)
-
- self._texture.bind()
-
- self._renderGL2PreDrawHook(ctx, program)
-
- self._draw(program)
-
- if not self.isBackfaceVisible:
- gl.glDisable(gl.GL_CULL_FACE)
-
-
-class ImageData(_Image):
- """Display a 2x2 data array with a texture."""
-
- _imageDecl = string.Template("""
- $colormapDecl
-
- vec4 imageColor(sampler2D data, vec2 texCoords) {
- float value = texture2D(data, texCoords).r;
- vec4 color = $colormapCall(value);
- return color;
- }
- """)
-
- def __init__(self, data, copy=True, colormap=None):
- super(ImageData, self).__init__(data, copy=copy)
-
- self._colormap = colormap or Colormap() # Default colormap
- self._colormap.addListener(self._cmapChanged)
-
- def setData(self, data, copy=True):
- data = numpy.array(data, copy=copy, order='C', dtype=numpy.float32)
- # TODO support (u)int8|16
- assert data.ndim == 2
-
- super(ImageData, self).setData(data, copy=False)
-
- @property
- def colormap(self):
- """The colormap used to render the image"""
- return self._colormap
-
- def _cmapChanged(self, source, *args, **kwargs):
- """Broadcast colormap changes"""
- self.notify(*args, **kwargs)
-
- def _textureFormat(self):
- return gl.GL_R32F, gl.GL_RED
-
- def _renderGL2PreDrawHook(self, ctx, program):
- self.colormap.setupProgram(ctx, program)
-
- def _shaderImageColorDecl(self):
- return self._imageDecl.substitute(
- colormapDecl=self.colormap.decl,
- colormapCall=self.colormap.call)
-
-
-# ImageRgba ##################################################################
-
-class ImageRgba(_Image):
- """Display a 2x2 RGBA image with a texture.
-
- Supports images of float in [0, 1] and uint8.
- """
-
- _imageDecl = """
- vec4 imageColor(sampler2D data, vec2 texCoords) {
- vec4 color = texture2D(data, texCoords);
- return color;
- }
- """
-
- def __init__(self, data, copy=True):
- super(ImageRgba, self).__init__(data, copy=copy)
-
- def setData(self, data, copy=True):
- data = numpy.array(data, copy=copy, order='C')
- assert data.ndim == 3
- assert data.shape[2] in (3, 4)
- if data.dtype.kind == 'f':
- if data.dtype != numpy.dtype(numpy.float32):
- _logger.warning("Converting image data to float32")
- data = numpy.array(data, dtype=numpy.float32, copy=False)
- else:
- assert data.dtype == numpy.dtype(numpy.uint8)
-
- super(ImageRgba, self).setData(data, copy=False)
-
- def _textureFormat(self):
- format_ = gl.GL_RGBA if self._data.shape[2] == 4 else gl.GL_RGB
- return format_, format_
-
- def _shaderImageColorDecl(self):
- return self._imageDecl
-
-
-# Group ######################################################################
-
-# TODO lighting, clipping as groups?
-# group composition?
-
-class GroupDepthOffset(core.Group):
- """A group using 2-pass rendering and glDepthRange to avoid Z-fighting"""
-
- def __init__(self, children=(), epsilon=None):
- super(GroupDepthOffset, self).__init__(children)
- self._epsilon = epsilon
- self.isDepthRangeOn = True
-
- def prepareGL2(self, ctx):
- if self._epsilon is None:
- depthbits = gl.glGetInteger(gl.GL_DEPTH_BITS)
- self._epsilon = 1. / (1 << (depthbits - 1))
-
- def renderGL2(self, ctx):
- if self.isDepthRangeOn:
- self._renderGL2WithDepthRange(ctx)
- else:
- super(GroupDepthOffset, self).renderGL2(ctx)
-
- def _renderGL2WithDepthRange(self, ctx):
- # gl.glDepthFunc(gl.GL_LESS)
- with gl.enabled(gl.GL_CULL_FACE):
- gl.glCullFace(gl.GL_BACK)
- for child in self.children:
- gl.glColorMask(
- gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE)
- gl.glDepthMask(gl.GL_TRUE)
- gl.glDepthRange(self._epsilon, 1.)
-
- child.render(ctx)
-
- gl.glColorMask(
- gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
- gl.glDepthMask(gl.GL_FALSE)
- gl.glDepthRange(0., 1. - self._epsilon)
-
- child.render(ctx)
-
- gl.glCullFace(gl.GL_FRONT)
- for child in reversed(self.children):
- gl.glColorMask(
- gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE)
- gl.glDepthMask(gl.GL_TRUE)
- gl.glDepthRange(self._epsilon, 1.)
-
- child.render(ctx)
-
- gl.glColorMask(
- gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
- gl.glDepthMask(gl.GL_FALSE)
- gl.glDepthRange(0., 1. - self._epsilon)
-
- child.render(ctx)
-
- gl.glDepthMask(gl.GL_TRUE)
- gl.glDepthRange(0., 1.)
- # gl.glDepthFunc(gl.GL_LEQUAL)
- # TODO use epsilon for all rendering?
- # TODO issue with picking in depth buffer!
-
-
-class GroupNoDepth(core.Group):
- """A group rendering its children without writing to the depth buffer
-
- :param bool mask: True (default) to disable writing in the depth buffer
- :param bool notest: True (default) to disable depth test
- """
-
- def __init__(self, children=(), mask=True, notest=True):
- super(GroupNoDepth, self).__init__(children)
- self._mask = bool(mask)
- self._notest = bool(notest)
-
- def renderGL2(self, ctx):
- if self._mask:
- gl.glDepthMask(gl.GL_FALSE)
-
- with gl.disabled(gl.GL_DEPTH_TEST, disable=self._notest):
- super(GroupNoDepth, self).renderGL2(ctx)
-
- if self._mask:
- gl.glDepthMask(gl.GL_TRUE)
-
-
-class GroupBBox(core.PrivateGroup):
- """A group displaying a bounding box around the children."""
-
- def __init__(self, children=(), color=(1., 1., 1., 1.)):
- super(GroupBBox, self).__init__()
- self._group = core.Group(children)
-
- self._boxTransforms = transform.TransformList((transform.Translate(),))
-
- # Using 1 of 3 primitives to render axes and/or bounding box
- # To avoid z-fighting between axes and bounding box
- self._boxWithAxes = BoxWithAxes(color)
- self._boxWithAxes.smooth = False
- self._boxWithAxes.transforms = self._boxTransforms
-
- self._box = Box(stroke=color, fill=(1., 1., 1., 0.))
- self._box.strokeSmooth = False
- self._box.transforms = self._boxTransforms
- self._box.visible = False
-
- self._axes = Axes()
- self._axes.smooth = False
- self._axes.transforms = self._boxTransforms
- self._axes.visible = False
-
- self.strokeWidth = 2.
-
- self._children = [self._boxWithAxes, self._box, self._axes, self._group]
-
- def _updateBoxAndAxes(self):
- """Update bbox and axes position and size according to children."""
- bounds = self._group.bounds(dataBounds=True)
- if bounds is not None:
- origin = bounds[0]
- size = bounds[1] - bounds[0]
- else:
- origin, size = (0., 0., 0.), (1., 1., 1.)
-
- self._boxTransforms[0].translation = origin
-
- self._boxWithAxes.size = size
- self._box.size = size
- self._axes.size = size
-
- def _bounds(self, dataBounds=False):
- self._updateBoxAndAxes()
- return super(GroupBBox, self)._bounds(dataBounds)
-
- def prepareGL2(self, ctx):
- self._updateBoxAndAxes()
- super(GroupBBox, self).prepareGL2(ctx)
-
- # Give access to _group children
-
- @property
- def children(self):
- return self._group.children
-
- @children.setter
- def children(self, iterable):
- self._group.children = iterable
-
- # Give access to box color and stroke width
-
- @property
- def color(self):
- """The RGBA color to use for the box: 4 float in [0, 1]"""
- return self._box.strokeColor
-
- @color.setter
- def color(self, color):
- self._box.strokeColor = color
- self._boxWithAxes.color = color
-
- @property
- def strokeWidth(self):
- """The width of the stroke lines in pixels (float)"""
- return self._box.strokeWidth
-
- @strokeWidth.setter
- def strokeWidth(self, width):
- width = float(width)
- self._box.strokeWidth = width
- self._boxWithAxes.width = width
- self._axes.width = width
-
- # Toggle axes visibility
-
- def _updateBoxAndAxesVisibility(self, axesVisible, boxVisible):
- """Update visible flags of box and axes primitives accordingly.
-
- :param bool axesVisible: True to display axes
- :param bool boxVisible: True to display bounding box
- """
- self._boxWithAxes.visible = boxVisible and axesVisible
- self._box.visible = boxVisible and not axesVisible
- self._axes.visible = not boxVisible and axesVisible
-
- @property
- def axesVisible(self):
- """Whether axes are displayed or not (bool)"""
- return self._boxWithAxes.visible or self._axes.visible
-
- @axesVisible.setter
- def axesVisible(self, visible):
- self._updateBoxAndAxesVisibility(axesVisible=bool(visible),
- boxVisible=self.boxVisible)
-
- @property
- def boxVisible(self):
- """Whether bounding box is displayed or not (bool)"""
- return self._boxWithAxes.visible or self._box.visible
-
- @boxVisible.setter
- def boxVisible(self, visible):
- self._updateBoxAndAxesVisibility(axesVisible=self.axesVisible,
- boxVisible=bool(visible))
-
-
-# Clipping Plane ##############################################################
-
-class ClipPlane(PlaneInGroup):
- """A clipping plane attached to a box"""
-
- def renderGL2(self, ctx):
- super(ClipPlane, self).renderGL2(ctx)
-
- if self.visible:
- # Set-up clipping plane for following brothers
-
- # No need of perspective divide, no projection
- point = ctx.objectToCamera.transformPoint(self.plane.point,
- perspectiveDivide=False)
- normal = ctx.objectToCamera.transformNormal(self.plane.normal)
- ctx.setClipPlane(point, normal)
-
- def postRender(self, ctx):
- if self.visible:
- # Disable clip planes
- ctx.setClipPlane()