diff options
Diffstat (limited to 'silx/gui/plot3d/items/mesh.py')
-rw-r--r-- | silx/gui/plot3d/items/mesh.py | 281 |
1 files changed, 205 insertions, 76 deletions
diff --git a/silx/gui/plot3d/items/mesh.py b/silx/gui/plot3d/items/mesh.py index 21936ea..d3f5e38 100644 --- a/silx/gui/plot3d/items/mesh.py +++ b/silx/gui/plot3d/items/mesh.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017-2018 European Synchrotron Radiation Facility +# Copyright (c) 2017-2019 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 @@ -35,17 +35,18 @@ __date__ = "17/07/2018" import logging import numpy -from ..scene import primitives, utils +from ..scene import primitives, utils, function from ..scene.transform import Rotate from .core import DataItem3D, ItemChangedType +from .mixins import ColormapMixIn from ._pick import PickingResult _logger = logging.getLogger(__name__) -class Mesh(DataItem3D): - """Description of mesh. +class _MeshBase(DataItem3D): + """Base class for :class:`Mesh' and :class:`ColormapMesh`. :param parent: The View widget this item belongs to. """ @@ -54,48 +55,22 @@ class Mesh(DataItem3D): DataItem3D.__init__(self, parent=parent) self._mesh = None - def setData(self, - position, - color, - normal=None, - mode='triangles', - copy=True): - """Set mesh geometry data. - - Supported drawing modes are: 'triangles', 'triangle_strip', 'fan' + def _setMesh(self, mesh): + """Set mesh primitive - :param numpy.ndarray position: - Position (x, y, z) of each vertex as a (N, 3) array - :param numpy.ndarray color: Colors for each point or a single color - :param numpy.ndarray normal: Normals for each point or None (default) - :param str mode: The drawing mode. - :param bool copy: True (default) to copy the data, - False to use as is (do not modify!). + :param Union[None,Geometry] mesh: The scene primitive """ self._getScenePrimitive().children = [] # Remove any previous mesh - if position is None or len(position) == 0: - self._mesh = None - else: - self._mesh = primitives.Mesh3D( - position, color, normal, mode=mode, copy=copy) + self._mesh = mesh + if self._mesh is not None: self._getScenePrimitive().children.append(self._mesh) - self.sigItemChanged.emit(ItemChangedType.DATA) + self._updated(ItemChangedType.DATA) - def getData(self, copy=True): - """Get the mesh geometry. - - :param bool copy: - True (default) to get a copy, - False to get internal representation (do not modify!). - :return: The positions, colors, normals and mode - :rtype: tuple of numpy.ndarray - """ - return (self.getPositionData(copy=copy), - self.getColorData(copy=copy), - self.getNormalData(copy=copy), - self.getDrawMode()) + def _getMesh(self): + """Returns the underlying Mesh scene primitive""" + return self._mesh def getPositionData(self, copy=True): """Get the mesh vertex positions. @@ -106,38 +81,38 @@ class Mesh(DataItem3D): :return: The (x, y, z) positions as a (N, 3) array :rtype: numpy.ndarray """ - if self._mesh is None: + if self._getMesh() is None: return numpy.empty((0, 3), dtype=numpy.float32) else: - return self._mesh.getAttribute('position', copy=copy) + return self._getMesh().getAttribute('position', copy=copy) - def getColorData(self, copy=True): - """Get the mesh vertex colors. + def getNormalData(self, copy=True): + """Get the mesh vertex normals. :param bool copy: True (default) to get a copy, False to get internal representation (do not modify!). - :return: The RGBA colors as a (N, 4) array or a single color - :rtype: numpy.ndarray + :return: The normals as a (N, 3) array, a single normal or None + :rtype: Union[numpy.ndarray,None] """ - if self._mesh is None: - return numpy.empty((0, 4), dtype=numpy.float32) + if self._getMesh() is None: + return None else: - return self._mesh.getAttribute('color', copy=copy) + return self._getMesh().getAttribute('normal', copy=copy) - def getNormalData(self, copy=True): - """Get the mesh vertex normals. + def getIndices(self, copy=True): + """Get the vertex indices. :param bool copy: True (default) to get a copy, False to get internal representation (do not modify!). - :return: The normals as a (N, 3) array, a single normal or None - :rtype: numpy.ndarray or None + :return: The vertex indices as an array or None. + :rtype: Union[numpy.ndarray,None] """ - if self._mesh is None: + if self._getMesh() is None: return None else: - return self._mesh.getAttribute('normal', copy=copy) + return self._getMesh().getIndices(copy=copy) def getDrawMode(self): """Get mesh rendering mode. @@ -145,7 +120,7 @@ class Mesh(DataItem3D): :return: The drawing mode of this primitive :rtype: str """ - return self._mesh.drawMode + return self._getMesh().drawMode def _pickFull(self, context): """Perform precise picking in this item at given widget position. @@ -164,28 +139,34 @@ class Mesh(DataItem3D): return None mode = self.getDrawMode() - if mode == 'triangles': - triangles = positions.reshape(-1, 3, 3) - - elif mode == 'triangle_strip': - # Expand strip - triangles = numpy.empty((len(positions) - 2, 3, 3), - dtype=positions.dtype) - triangles[:, 0] = positions[:-2] - triangles[:, 1] = positions[1:-1] - triangles[:, 2] = positions[2:] - - elif mode == 'fan': - # Expand fan - triangles = numpy.empty((len(positions) - 2, 3, 3), - dtype=positions.dtype) - triangles[:, 0] = positions[0] - triangles[:, 1] = positions[1:-1] - triangles[:, 2] = positions[2:] + vertexIndices = self.getIndices(copy=False) + if vertexIndices is not None: # Expand indices + positions = utils.unindexArrays(mode, vertexIndices, positions)[0] + triangles = positions.reshape(-1, 3, 3) else: - _logger.warning("Unsupported draw mode: %s" % mode) - return None + if mode == 'triangles': + triangles = positions.reshape(-1, 3, 3) + + elif mode == 'triangle_strip': + # Expand strip + triangles = numpy.empty((len(positions) - 2, 3, 3), + dtype=positions.dtype) + triangles[:, 0] = positions[:-2] + triangles[:, 1] = positions[1:-1] + triangles[:, 2] = positions[2:] + + elif mode == 'fan': + # Expand fan + triangles = numpy.empty((len(positions) - 2, 3, 3), + dtype=positions.dtype) + triangles[:, 0] = positions[0] + triangles[:, 1] = positions[1:-1] + triangles[:, 2] = positions[2:] + + else: + _logger.warning("Unsupported draw mode: %s" % mode) + return None trianglesIndices, t, barycentric = utils.segmentTrianglesIntersection( rayObject, triangles) @@ -208,12 +189,160 @@ class Mesh(DataItem3D): indices = trianglesIndices + closest # For corners 1 and 2 indices[closest == 0] = 0 # For first corner (common) + if vertexIndices is not None: + # Convert from indices in expanded triangles to input vertices + indices = vertexIndices[indices] + return PickingResult(self, positions=points, indices=indices, fetchdata=self.getPositionData) +class Mesh(_MeshBase): + """Description of mesh. + + :param parent: The View widget this item belongs to. + """ + + def __init__(self, parent=None): + _MeshBase.__init__(self, parent=parent) + + def setData(self, + position, + color, + normal=None, + mode='triangles', + indices=None, + copy=True): + """Set mesh geometry data. + + Supported drawing modes are: 'triangles', 'triangle_strip', 'fan' + + :param numpy.ndarray position: + Position (x, y, z) of each vertex as a (N, 3) array + :param numpy.ndarray color: Colors for each point or a single color + :param Union[numpy.ndarray,None] normal: Normals for each point or None (default) + :param str mode: The drawing mode. + :param Union[List[int],None] indices: + Array of vertex indices or None to use arrays directly. + :param bool copy: True (default) to copy the data, + False to use as is (do not modify!). + """ + assert mode in ('triangles', 'triangle_strip', 'fan') + if position is None or len(position) == 0: + mesh = None + else: + mesh = primitives.Mesh3D( + position, color, normal, mode=mode, indices=indices, copy=copy) + self._setMesh(mesh) + + def getData(self, copy=True): + """Get the mesh geometry. + + :param bool copy: + True (default) to get a copy, + False to get internal representation (do not modify!). + :return: The positions, colors, normals and mode + :rtype: tuple of numpy.ndarray + """ + return (self.getPositionData(copy=copy), + self.getColorData(copy=copy), + self.getNormalData(copy=copy), + self.getDrawMode()) + + def getColorData(self, copy=True): + """Get the mesh vertex colors. + + :param bool copy: + True (default) to get a copy, + False to get internal representation (do not modify!). + :return: The RGBA colors as a (N, 4) array or a single color + :rtype: numpy.ndarray + """ + if self._getMesh() is None: + return numpy.empty((0, 4), dtype=numpy.float32) + else: + return self._getMesh().getAttribute('color', copy=copy) + + +class ColormapMesh(_MeshBase, ColormapMixIn): + """Description of mesh which color is defined by scalar and a colormap. + + :param parent: The View widget this item belongs to. + """ + + def __init__(self, parent=None): + _MeshBase.__init__(self, parent=parent) + ColormapMixIn.__init__(self, function.Colormap()) + + def setData(self, + position, + value, + normal=None, + mode='triangles', + indices=None, + copy=True): + """Set mesh geometry data. + + Supported drawing modes are: 'triangles', 'triangle_strip', 'fan' + + :param numpy.ndarray position: + Position (x, y, z) of each vertex as a (N, 3) array + :param numpy.ndarray value: Data value for each vertex. + :param Union[numpy.ndarray,None] normal: Normals for each point or None (default) + :param str mode: The drawing mode. + :param Union[List[int],None] indices: + Array of vertex indices or None to use arrays directly. + :param bool copy: True (default) to copy the data, + False to use as is (do not modify!). + """ + assert mode in ('triangles', 'triangle_strip', 'fan') + if position is None or len(position) == 0: + mesh = None + else: + mesh = primitives.ColormapMesh3D( + position=position, + value=numpy.array(value, copy=False).reshape(-1, 1), # Make it a 2D array + colormap=self._getSceneColormap(), + normal=normal, + mode=mode, + indices=indices, + copy=copy) + self._setMesh(mesh) + + # Store data range info + ColormapMixIn._setRangeFromData(self, self.getValueData(copy=False)) + + def getData(self, copy=True): + """Get the mesh geometry. + + :param bool copy: + True (default) to get a copy, + False to get internal representation (do not modify!). + :return: The positions, values, normals and mode + :rtype: tuple of numpy.ndarray + """ + return (self.getPositionData(copy=copy), + self.getValueData(copy=copy), + self.getNormalData(copy=copy), + self.getDrawMode()) + + def getValueData(self, copy=True): + """Get the mesh vertex values. + + :param bool copy: + True (default) to get a copy, + False to get internal representation (do not modify!). + :return: Array of data values + :rtype: numpy.ndarray + """ + if self._getMesh() is None: + return numpy.empty((0,), dtype=numpy.float32) + else: + return self._getMesh().getAttribute('value', copy=copy) + + class _CylindricalVolume(DataItem3D): """Class that represents a volume with a rotational symmetry along z @@ -345,7 +474,7 @@ class _CylindricalVolume(DataItem3D): vertices, color, normals, mode='triangles', copy=False) self._getScenePrimitive().children.append(self._mesh) - self.sigItemChanged.emit(ItemChangedType.DATA) + self._updated(ItemChangedType.DATA) def _pickFull(self, context): """Perform precise picking in this item at given widget position. |