diff options
Diffstat (limited to 'silx/gui/plot3d/items')
-rw-r--r-- | silx/gui/plot3d/items/__init__.py | 4 | ||||
-rw-r--r-- | silx/gui/plot3d/items/core.py | 4 | ||||
-rw-r--r-- | silx/gui/plot3d/items/mesh.py | 281 | ||||
-rw-r--r-- | silx/gui/plot3d/items/mixins.py | 21 | ||||
-rw-r--r-- | silx/gui/plot3d/items/scatter.py | 39 | ||||
-rw-r--r-- | silx/gui/plot3d/items/volume.py | 12 |
6 files changed, 253 insertions, 108 deletions
diff --git a/silx/gui/plot3d/items/__init__.py b/silx/gui/plot3d/items/__init__.py index b2a9dab..58eee9c 100644 --- a/silx/gui/plot3d/items/__init__.py +++ b/silx/gui/plot3d/items/__init__.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 @@ -38,6 +38,6 @@ from .mixins import (ColormapMixIn, InterpolationMixIn, # noqa PlaneMixIn, SymbolMixIn) # noqa from .clipplane import ClipPlane # noqa from .image import ImageData, ImageRgba # noqa -from .mesh import Mesh, Box, Cylinder, Hexagon # noqa +from .mesh import Mesh, ColormapMesh, Box, Cylinder, Hexagon # noqa from .scatter import Scatter2D, Scatter3D # noqa from .volume import ScalarField3D # noqa diff --git a/silx/gui/plot3d/items/core.py b/silx/gui/plot3d/items/core.py index 0aefced..1745b2b 100644 --- a/silx/gui/plot3d/items/core.py +++ b/silx/gui/plot3d/items/core.py @@ -32,10 +32,10 @@ __license__ = "MIT" __date__ = "15/11/2017" from collections import defaultdict +import enum import numpy - -from silx.third_party import enum, six +import six from ... import qt from ...plot.items import ItemChangedType 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. diff --git a/silx/gui/plot3d/items/mixins.py b/silx/gui/plot3d/items/mixins.py index 8e96441..40b8438 100644 --- a/silx/gui/plot3d/items/mixins.py +++ b/silx/gui/plot3d/items/mixins.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 @@ -114,19 +114,17 @@ class ColormapMixIn(_ColormapMixIn): self.__sceneColormap = sceneColormap self._syncSceneColormap() - self.sigItemChanged.connect(self.__colormapUpdated) - - def __colormapUpdated(self, event): + def _colormapChanged(self): """Handle colormap updates""" - if event == ItemChangedType.COLORMAP: - self._syncSceneColormap() + self._syncSceneColormap() + super(ColormapMixIn, self)._colormapChanged() def _setRangeFromData(self, data=None): """Compute the data range the colormap should use from provided data. :param data: Data set from which to compute the range or None """ - if data is None or len(data) == 0: + if data is None or data.size == 0: dataRange = None else: dataRange = min_max(data, min_positive=True, finite=True) @@ -144,6 +142,13 @@ class ColormapMixIn(_ColormapMixIn): if self.getColormap().isAutoscale(): self._syncSceneColormap() + def _getDataRange(self): + """Returns the data range as used in the scene for colormap + + :rtype: Union[List[float],None] + """ + return self._dataRange + def _setSceneColormap(self, sceneColormap): """Set the scene colormap to sync with Colormap object. @@ -171,8 +176,6 @@ class ColormapMixIn(_ColormapMixIn): class SymbolMixIn(_SymbolMixIn): """Mix-in class for symbol and symbolSize properties for Item3D""" - _DEFAULT_SYMBOL = 'o' - _DEFAULT_SYMBOL_SIZE = 7.0 _SUPPORTED_SYMBOLS = collections.OrderedDict(( ('o', 'Circle'), ('d', 'Diamond'), diff --git a/silx/gui/plot3d/items/scatter.py b/silx/gui/plot3d/items/scatter.py index a13c3db..b7bcd09 100644 --- a/silx/gui/plot3d/items/scatter.py +++ b/silx/gui/plot3d/items/scatter.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 @@ -36,6 +36,7 @@ import logging import sys import numpy +from ....utils.deprecation import deprecated from ..scene import function, primitives, utils from .core import DataItem3D, Item3DChangedType, ItemChangedType @@ -43,7 +44,7 @@ from .mixins import ColormapMixIn, SymbolMixIn from ._pick import PickingResult -_logger = logging.getLevelName(__name__) +_logger = logging.getLogger(__name__) class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn): @@ -94,7 +95,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn): self._scatter.setAttribute('z', z, copy=copy) self._scatter.setAttribute('value', value, copy=copy) - ColormapMixIn._setRangeFromData(self, self.getValues(copy=False)) + ColormapMixIn._setRangeFromData(self, self.getValueData(copy=False)) self._updated(ItemChangedType.DATA) def getData(self, copy=True): @@ -107,7 +108,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn): return (self.getXData(copy), self.getYData(copy), self.getZData(copy), - self.getValues(copy)) + self.getValueData(copy)) def getXData(self, copy=True): """Returns X data coordinates. @@ -139,7 +140,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn): """ return self._scatter.getAttribute('z', copy=copy).reshape(-1) - def getValues(self, copy=True): + def getValueData(self, copy=True): """Returns data values. :param bool copy: True to get a copy, @@ -149,6 +150,11 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn): """ return self._scatter.getAttribute('value', copy=copy).reshape(-1) + @deprecated(reason="Consistency with PlotWidget items", + replacement="getValueData", since_version="0.10.0") + def getValues(self, copy=True): + return self.getValueData(copy) + def _pickFull(self, context, threshold=0., sort='depth'): """Perform picking in this item at given widget position. @@ -202,7 +208,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn): return PickingResult(self, positions=dataPoints[picked, :3], indices=picked, - fetchdata=self.getValues) + fetchdata=self.getValueData) else: return None @@ -269,8 +275,8 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn): Supported visualization modes are: - 'points': For scatter plot representation - - 'lines': For Delaunay tesselation-based wireframe representation - - 'solid': For Delaunay tesselation-based solid surface representation + - 'lines': For Delaunay tessellation-based wireframe representation + - 'solid': For Delaunay tessellation-based solid surface representation :param str mode: Mode of representation to use """ @@ -384,7 +390,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn): self._cachedTrianglesIndices = None # Store data range info - ColormapMixIn._setRangeFromData(self, self.getValues(copy=False)) + ColormapMixIn._setRangeFromData(self, self.getValueData(copy=False)) self._updateScene() @@ -399,7 +405,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn): """ return (self.getXData(copy=copy), self.getYData(copy=copy), - self.getValues(copy=copy)) + self.getValueData(copy=copy)) def getXData(self, copy=True): """Returns X data coordinates. @@ -421,7 +427,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn): """ return numpy.array(self._y, copy=copy) - def getValues(self, copy=True): + def getValueData(self, copy=True): """Returns data values. :param bool copy: True to get a copy, @@ -431,6 +437,11 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn): """ return numpy.array(self._value, copy=copy) + @deprecated(reason="Consistency with PlotWidget items", + replacement="getValueData", since_version="0.10.0") + def getValues(self, copy=True): + return self.getValueData(copy) + def _pickPoints(self, context, points, threshold=1., sort='depth'): """Perform picking while in 'points' visualization mode @@ -472,7 +483,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn): return PickingResult(self, positions=points[picked, :3], indices=picked, - fetchdata=self.getValues) + fetchdata=self.getValueData) else: return None @@ -507,7 +518,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn): return PickingResult(self, positions=positions, indices=indices, - fetchdata=self.getValues) + fetchdata=self.getValueData) def _pickFull(self, context): """Perform picking in this item at given widget position. @@ -521,7 +532,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn): return None if self.isHeightMap(): - zData = self.getValues(copy=False) + zData = self.getValueData(copy=False) else: zData = numpy.zeros_like(xData) diff --git a/silx/gui/plot3d/items/volume.py b/silx/gui/plot3d/items/volume.py index ca22f1f..08ad02a 100644 --- a/silx/gui/plot3d/items/volume.py +++ b/silx/gui/plot3d/items/volume.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 @@ -78,13 +78,15 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn): def _parentChanged(self, event): """Handle data change in the parent this plane belongs to""" if event == ItemChangedType.DATA: - self._getPlane().setData(self.sender().getData(copy=False), - copy=False) + data = self.sender().getData(copy=False) + self._getPlane().setData(data, copy=False) # Store data range info as 3-tuple of values self._dataRange = self.sender().getDataRange() + self._setRangeFromData( + None if self._dataRange is None else numpy.array(self._dataRange)) - self.sigItemChanged.emit(ItemChangedType.DATA) + self._updated(ItemChangedType.DATA) # Colormap @@ -104,7 +106,7 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn): display = bool(display) if display != self.getDisplayValuesBelowMin(): self._getPlane().colormap.displayValuesBelowMin = display - self.sigItemChanged.emit(ItemChangedType.ALPHA) + self._updated(ItemChangedType.ALPHA) def getDataRange(self): """Return the range of the data as a 3-tuple of values. |