diff options
Diffstat (limited to 'silx/gui/plot3d')
-rw-r--r-- | silx/gui/plot3d/_model/items.py | 96 | ||||
-rw-r--r-- | silx/gui/plot3d/items/_pick.py | 39 | ||||
-rw-r--r-- | silx/gui/plot3d/items/scatter.py | 3 | ||||
-rw-r--r-- | silx/gui/plot3d/items/volume.py | 151 | ||||
-rw-r--r-- | silx/gui/plot3d/scene/primitives.py | 9 | ||||
-rw-r--r-- | silx/gui/plot3d/tools/PositionInfoWidget.py | 2 |
6 files changed, 230 insertions, 70 deletions
diff --git a/silx/gui/plot3d/_model/items.py b/silx/gui/plot3d/_model/items.py index 9fe3e51..7f3921a 100644 --- a/silx/gui/plot3d/_model/items.py +++ b/silx/gui/plot3d/_model/items.py @@ -45,7 +45,7 @@ from ...utils.image import convertArrayToQImage from ...colors import preferredColormaps from ... import qt, icons from .. import items -from ..items.volume import Isosurface, CutPlane +from ..items.volume import Isosurface, CutPlane, ComplexIsosurface from ..Plot3DWidget import Plot3DWidget @@ -867,6 +867,17 @@ class ColormapRow(_ColormapBaseProxyRow): self._sigColormapChanged.connect(self._updateColormapImage) + def getColormapImage(self): + """Returns image representing the colormap or None + + :rtype: Union[QImage,None] + """ + if self._colormapImage is None and self._colormap is not None: + image = numpy.zeros((16, 130, 3), dtype=numpy.uint8) + image[1:-1, 1:-1] = self._colormap.getNColors(image.shape[1] - 2)[:, :3] + self._colormapImage = convertArrayToQImage(image) + return self._colormapImage + def _get(self): """Getter for ProxyRow subclass""" return None @@ -908,13 +919,9 @@ class ColormapRow(_ColormapBaseProxyRow): def data(self, column, role): if column == 1 and role == qt.Qt.DecorationRole: - if self._colormapImage is None: - image = numpy.zeros((16, 130, 3), dtype=numpy.uint8) - image[1:-1, 1:-1] = self._colormap.getNColors(image.shape[1] - 2)[:, :3] - self._colormapImage = convertArrayToQImage(image) - return self._colormapImage - - return super(ColormapRow, self).data(column, role) + return self.getColormapImage() + else: + return super(ColormapRow, self).data(column, role) class SymbolRow(ItemProxyRow): @@ -1055,12 +1062,12 @@ class ComplexModeRow(ItemProxyRow): :param Item3D item: Scene item with symbol property """ - def __init__(self, item): + def __init__(self, item, name='Mode'): names = [m.value.replace('_', ' ').title() for m in item.supportedComplexModes()] super(ComplexModeRow, self).__init__( item=item, - name='Mode', + name=name, fget=item.getComplexMode, fset=item.setComplexMode, events=items.ItemChangedType.COMPLEX_MODE, @@ -1283,6 +1290,71 @@ class IsosurfaceRow(Item3DRow): return super(IsosurfaceRow, self).setData(column, value, role) +class ComplexIsosurfaceRow(IsosurfaceRow): + """Represents an :class:`ComplexIsosurface` item. + + :param ComplexIsosurface item: + """ + + _EVENTS = (items.ItemChangedType.VISIBLE, + items.ItemChangedType.COLOR, + items.ItemChangedType.COMPLEX_MODE) + """Events for which to update the first column in the tree""" + + def __init__(self, item): + super(ComplexIsosurfaceRow, self).__init__(item) + + self.addRow(ComplexModeRow(item, "Color Complex Mode"), index=1) + for row in self.children(): + if isinstance(row, ColorProxyRow): + self._colorRow = row + break + else: + raise RuntimeError("Cannot retrieve Color tree row") + self._colormapRow = ColormapRow(item) + + self.__updateRowsForItem(item) + item.sigItemChanged.connect(self.__itemChanged) + + def __itemChanged(self, event): + """Update enabled/disabled rows""" + if event == items.ItemChangedType.COMPLEX_MODE: + item = self.sender() + self.__updateRowsForItem(item) + + def __updateRowsForItem(self, item): + """Update rows for item + + :param item: + """ + if not isinstance(item, ComplexIsosurface): + return + + if item.getComplexMode() == items.ComplexMixIn.ComplexMode.NONE: + removed = self._colormapRow + added = self._colorRow + else: + removed = self._colorRow + added = self._colormapRow + + # Remove unwanted rows + if removed in self.children(): + self.removeRow(removed) + + # Add required rows + if added not in self.children(): + self.addRow(added, index=2) + + def data(self, column, role): + if column == 0 and role == qt.Qt.DecorationRole: + item = self.item() + if (item is not None and + item.getComplexMode() != items.ComplexMixIn.ComplexMode.NONE): + return self._colormapRow.getColormapImage() + + return super(ComplexIsosurfaceRow, self).data(column, role) + + class AddIsosurfaceRow(BaseRow): """Class for Isosurface create button @@ -1358,7 +1430,7 @@ class VolumeIsoSurfacesRow(StaticRow): volume.sigIsosurfaceRemoved.connect(self._isosurfaceRemoved) if isinstance(volume, items.ComplexMixIn): - self.addRow(ComplexModeRow(volume)) + self.addRow(ComplexModeRow(volume, "Complex Mode")) for item in volume.getIsosurfaces(): self.addRow(nodeFromItem(item)) @@ -1581,6 +1653,8 @@ def nodeFromItem(item): # Item with specific model row class if isinstance(item, (items.GroupItem, items.GroupWithAxesItem)): return GroupItemRow(item) + elif isinstance(item, ComplexIsosurface): + return ComplexIsosurfaceRow(item) elif isinstance(item, Isosurface): return IsosurfaceRow(item) diff --git a/silx/gui/plot3d/items/_pick.py b/silx/gui/plot3d/items/_pick.py index b35ef0d..8494723 100644 --- a/silx/gui/plot3d/items/_pick.py +++ b/silx/gui/plot3d/items/_pick.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2018 European Synchrotron Radiation Facility +# Copyright (c) 2018-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 @@ -34,6 +34,7 @@ __date__ = "24/09/2018" import logging import numpy +from ...plot.items._pick import PickingResult as _PickingResult from ..scene import Viewport, Base @@ -177,9 +178,8 @@ class PickContext(object): return rayObject -class PickingResult(object): - """Class to access picking information in a 3D scene. - """ +class PickingResult(_PickingResult): + """Class to access picking information in a 3D scene.""" def __init__(self, item, positions, indices=None, fetchdata=None): """Init @@ -194,7 +194,8 @@ class PickingResult(object): to provide an alternative function to access item data. Default is to use `item.getData`. """ - self._item = item + super(PickingResult, self).__init__(item, indices) + self._objectPositions = numpy.array( positions, copy=False, dtype=numpy.float) @@ -205,36 +206,8 @@ class PickingResult(object): self._scenePositions = None self._ndcPositions = None - if indices is None: - self._indices = None - else: - self._indices = numpy.array(indices, copy=False, dtype=numpy.int) - self._fetchdata = fetchdata - def getItem(self): - """Returns the item this results corresponds to. - - :rtype: ~silx.gui.plot3d.items.Item3D - """ - return self._item - - def getIndices(self, copy=True): - """Returns indices of picked data. - - If data is 1D, it returns a numpy.ndarray, otherwise - it returns a tuple with as many numpy.ndarray as there are - dimensions in the data. - - :param bool copy: True (default) to get a copy, - False to return internal arrays - :rtype: Union[None,numpy.ndarray,List[numpy.ndarray]] - """ - if self._indices is None: - return None - indices = numpy.array(self._indices, copy=copy) - return indices if indices.ndim == 1 else tuple(indices) - def getData(self, copy=True): """Returns picked data values diff --git a/silx/gui/plot3d/items/scatter.py b/silx/gui/plot3d/items/scatter.py index e8ffee1..5fce629 100644 --- a/silx/gui/plot3d/items/scatter.py +++ b/silx/gui/plot3d/items/scatter.py @@ -234,6 +234,9 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn, } """Dict {visualization mode: property names used in this mode}""" + _SUPPORTED_SCATTER_VISUALIZATION = tuple(_VISUALIZATION_PROPERTIES.keys()) + """Overrides supported Visualizations""" + def __init__(self, parent=None): DataItem3D.__init__(self, parent=parent) ColormapMixIn.__init__(self) diff --git a/silx/gui/plot3d/items/volume.py b/silx/gui/plot3d/items/volume.py index ae91e82..e0a2a1f 100644 --- a/silx/gui/plot3d/items/volume.py +++ b/silx/gui/plot3d/items/volume.py @@ -37,13 +37,14 @@ import numpy from silx.math.combo import min_max from silx.math.marchingcubes import MarchingCubes +from silx.math.interpolate import interp3d from ....utils.proxy import docstring from ... import _glutils as glu from ... import qt from ...colors import rgba -from ..scene import cutplane, primitives, transform, utils +from ..scene import cutplane, function, primitives, transform, utils from .core import BaseNodeItem, Item3D, ItemChangedType, Item3DChangedType from .mixins import ColormapMixIn, ComplexMixIn, InterpolationMixIn, PlaneMixIn @@ -301,6 +302,15 @@ class Isosurface(Item3D): """Return the color of this iso-surface (QColor)""" return qt.QColor.fromRgbF(*self._color) + def _updateColor(self, color): + """Handle update of color + + :param List[float] color: RGBA channels in [0, 1] + """ + primitive = self._getScenePrimitive() + if len(primitive.children) != 0: + primitive.children[0].setAttribute('color', color) + def setColor(self, color): """Set the color of the iso-surface @@ -310,15 +320,15 @@ class Isosurface(Item3D): color = rgba(color) if color != self._color: self._color = color - primitive = self._getScenePrimitive() - if len(primitive.children) != 0: - primitive.children[0].setAttribute('color', self._color) + self._updateColor(self._color) self._updated(ItemChangedType.COLOR) - def _updateScenePrimitive(self): - """Update underlying mesh""" - self._getScenePrimitive().children = [] + def _computeIsosurface(self): + """Compute isosurface for current state. + :return: (vertices, normals, indices) arrays + :rtype: List[Union[None,numpy.ndarray]] + """ data = self.getData(copy=False) if data is None: @@ -349,24 +359,31 @@ class Isosurface(Item3D): self._level = level self._updated(Item3DChangedType.ISO_LEVEL) - if not numpy.isfinite(self._level): - return + if numpy.isfinite(self._level): + st = time.time() + vertices, normals, indices = MarchingCubes( + data, + isolevel=self._level) + _logger.info('Computed iso-surface in %f s.', time.time() - st) - st = time.time() - vertices, normals, indices = MarchingCubes( - data, - isolevel=self._level) - _logger.info('Computed iso-surface in %f s.', time.time() - st) + if len(vertices) != 0: + return vertices, normals, indices - if len(vertices) == 0: - return - else: - mesh = primitives.Mesh3D(vertices, - colors=self._color, - normals=normals, - mode='triangles', - indices=indices) - self._getScenePrimitive().children = [mesh] + return None, None, None + + def _updateScenePrimitive(self): + """Update underlying mesh""" + self._getScenePrimitive().children = [] + + vertices, normals, indices = self._computeIsosurface() + if vertices is not None: + mesh = primitives.Mesh3D(vertices, + colors=self._color, + normals=normals, + mode='triangles', + indices=indices, + copy=False) + self._getScenePrimitive().children = [mesh] def _pickFull(self, context): """Perform picking in this item at given widget position. @@ -677,17 +694,39 @@ class ComplexCutPlane(CutPlane, ComplexMixIn): super(ComplexCutPlane, self)._updated(event) -class ComplexIsosurface(Isosurface): +class ComplexIsosurface(Isosurface, ComplexMixIn, ColormapMixIn): """Class representing an iso-surface in a :class:`ComplexField3D` item. :param parent: The DataItem3D this iso-surface belongs to """ + _SUPPORTED_COMPLEX_MODES = \ + (ComplexMixIn.ComplexMode.NONE,) + ComplexMixIn._SUPPORTED_COMPLEX_MODES + """Overrides supported ComplexMode""" + def __init__(self, parent): - super(ComplexIsosurface, self).__init__(parent) + ComplexMixIn.__init__(self) + ColormapMixIn.__init__(self, function.Colormap()) + Isosurface.__init__(self, parent=parent) + self.setComplexMode(self.ComplexMode.NONE) + + def _updateColor(self, color): + """Handle update of color + + :param List[float] color: RGBA channels in [0, 1] + """ + primitive = self._getScenePrimitive() + if (len(primitive.children) != 0 and + isinstance(primitive.children[0], primitives.ColormapMesh3D)): + primitive.children[0].alpha = self._color[3] + else: + super(ComplexIsosurface, self)._updateColor(color) def _syncDataWithParent(self): """Synchronize this instance data with that of its parent""" + if self.getComplexMode() != self.ComplexMode.NONE: + self._setRangeFromData(self.getColormappedData(copy=False)) + parent = self.parent() if parent is None: self._data = None @@ -702,6 +741,67 @@ class ComplexIsosurface(Isosurface): self._syncDataWithParent() super(ComplexIsosurface, self)._parentChanged(event) + def getColormappedData(self, copy=True): + """Return 3D dataset used to apply the colormap on the isosurface. + + This depends on :meth:`getComplexMode`. + + :param bool copy: + True (default) to get a copy, + False to get the internal data (DO NOT modify!) + :return: The data set (or None if not set) + :rtype: Union[numpy.ndarray,None] + """ + if self.getComplexMode() == self.ComplexMode.NONE: + return None + else: + parent = self.parent() + if parent is None: + return None + else: + return parent.getData(mode=self.getComplexMode(), copy=copy) + + def _updated(self, event=None): + """Handle update of the isosurface (and take care of mode change) + + :param ItemChangedType event: The kind of update + """ + if (event == ItemChangedType.COMPLEX_MODE and + self.getComplexMode() != self.ComplexMode.NONE): + self._setRangeFromData(self.getColormappedData(copy=False)) + + if event in (ItemChangedType.COMPLEX_MODE, + ItemChangedType.COLORMAP, + Item3DChangedType.INTERPOLATION): + self._updateScenePrimitive() + super(ComplexIsosurface, self)._updated(event) + + def _updateScenePrimitive(self): + """Update underlying mesh""" + if self.getComplexMode() == self.ComplexMode.NONE: + super(ComplexIsosurface, self)._updateScenePrimitive() + + else: # Specific display for colormapped isosurface + self._getScenePrimitive().children = [] + + values = self.getColormappedData(copy=False) + if values is not None: + vertices, normals, indices = self._computeIsosurface() + if vertices is not None: + values = interp3d(values, vertices, method='linear_omp') + # TODO reuse isosurface when only color changes... + + mesh = primitives.ColormapMesh3D( + vertices, + value=values.reshape(-1, 1), + colormap=self._getSceneColormap(), + normal=normals, + mode='triangles', + indices=indices, + copy=False) + mesh.alpha = self._color[3] + self._getScenePrimitive().children = [mesh] + class ComplexField3D(ScalarField3D, ComplexMixIn): """3D complex field on a regular grid. @@ -720,6 +820,7 @@ class ComplexField3D(ScalarField3D, ComplexMixIn): @docstring(ComplexMixIn) def setComplexMode(self, mode): + mode = ComplexMixIn.ComplexMode.from_value(mode) if mode != self.getComplexMode(): self.clearIsosurfaces() # Reset isosurfaces ComplexMixIn.setComplexMode(self, mode) diff --git a/silx/gui/plot3d/scene/primitives.py b/silx/gui/plot3d/scene/primitives.py index 08724ba..7db61e8 100644 --- a/silx/gui/plot3d/scene/primitives.py +++ b/silx/gui/plot3d/scene/primitives.py @@ -1874,6 +1874,8 @@ class ColormapMesh3D(Geometry): } """, string.Template(""" + uniform float alpha; + varying vec4 vCameraPosition; varying vec3 vPosition; varying vec3 vNormal; @@ -1889,6 +1891,7 @@ class ColormapMesh3D(Geometry): vec4 color = $colormapCall(vValue); gl_FragColor = $lightingCall(color, vPosition, vNormal); + gl_FragColor.a *= alpha; $scenePostCall(vCameraPosition); } @@ -1908,6 +1911,7 @@ class ColormapMesh3D(Geometry): value=value, copy=copy) + self._alpha = 1.0 self._lineWidth = 1.0 self._lineSmooth = True self._culling = None @@ -1922,6 +1926,10 @@ class ColormapMesh3D(Geometry): 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) @@ -1978,6 +1986,7 @@ class ColormapMesh3D(Geometry): 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) diff --git a/silx/gui/plot3d/tools/PositionInfoWidget.py b/silx/gui/plot3d/tools/PositionInfoWidget.py index fc86a7f..52a6163 100644 --- a/silx/gui/plot3d/tools/PositionInfoWidget.py +++ b/silx/gui/plot3d/tools/PositionInfoWidget.py @@ -189,7 +189,7 @@ class PositionInfoWidget(qt.QWidget): return # No picked item item = picking.getItem() - self._itemLabel.setText(item.getLabel()) + self._itemLabel.setText(item.getName()) positions = picking.getPositions('scene', copy=False) x, y, z = positions[0] self._xLabel.setText("%g" % x) |